Selaa lähdekoodia

Merge branch 'master' into ef-cleanup

# Conflicts:
#	Jellyfin.Data/Entities/Libraries/MediaFileStream.cs
#	Jellyfin.Data/Jellyfin.Data.csproj
Patrick Barron 4 vuotta sitten
vanhempi
sitoutus
e5380c653b
100 muutettua tiedostoa jossa 758 lisäystä ja 579 poistoa
  1. 1 1
      .ci/azure-pipelines-test.yml
  2. 1 0
      CONTRIBUTORS.md
  3. 9 3
      Emby.Dlna/DlnaManager.cs
  4. 0 1
      Emby.Dlna/Emby.Dlna.csproj
  5. 1 1
      Emby.Dlna/Main/DlnaEntryPoint.cs
  6. 1 1
      Emby.Dlna/PlayTo/PlayToController.cs
  7. 8 8
      Emby.Dlna/PlayTo/SsdpHttpClient.cs
  8. 2 4
      Emby.Dlna/PlayTo/TransportCommands.cs
  9. 3 0
      Emby.Dlna/Profiles/DefaultProfile.cs
  10. 0 1
      Emby.Drawing/Emby.Drawing.csproj
  11. 0 1
      Emby.Naming/Emby.Naming.csproj
  12. 9 2
      Emby.Naming/Video/CleanStringParser.cs
  13. 9 7
      Emby.Naming/Video/VideoListResolver.cs
  14. 2 1
      Emby.Naming/Video/VideoResolver.cs
  15. 0 1
      Emby.Notifications/Emby.Notifications.csproj
  16. 0 1
      Emby.Photos/Emby.Photos.csproj
  17. 2 1
      Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
  18. 27 15
      Emby.Server.Implementations/ApplicationHost.cs
  19. 1 1
      Emby.Server.Implementations/Channels/ChannelManager.cs
  20. 14 1
      Emby.Server.Implementations/Collections/CollectionManager.cs
  21. 1 1
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  22. 0 1
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  23. 11 0
      Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
  24. 1 1
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  25. 6 6
      Emby.Server.Implementations/IStartupOptions.cs
  26. 12 60
      Emby.Server.Implementations/Library/LibraryManager.cs
  27. 1 1
      Emby.Server.Implementations/Library/LiveStreamHelper.cs
  28. 1 1
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  29. 63 0
      Emby.Server.Implementations/Library/PathExtensions.cs
  30. 4 2
      Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
  31. 4 2
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  32. 1 1
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  33. 1 1
      Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
  34. 1 1
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  35. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
  36. 70 186
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
  37. 23 1
      Emby.Server.Implementations/Localization/Core/gl.json
  38. 1 1
      Emby.Server.Implementations/Localization/Core/hu.json
  39. 2 2
      Emby.Server.Implementations/Localization/Core/kk.json
  40. 3 2
      Emby.Server.Implementations/Localization/Core/th.json
  41. 1 1
      Emby.Server.Implementations/Localization/LocalizationManager.cs
  42. 4 4
      Emby.Server.Implementations/Plugins/PluginManager.cs
  43. 1 1
      Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
  44. 1 1
      Emby.Server.Implementations/Udp/UdpServer.cs
  45. 1 1
      Emby.Server.Implementations/Updates/InstallationManager.cs
  46. 4 4
      Jellyfin.Api/Controllers/AudioController.cs
  47. 1 1
      Jellyfin.Api/Controllers/ConfigurationController.cs
  48. 2 2
      Jellyfin.Api/Controllers/DashboardController.cs
  49. 24 24
      Jellyfin.Api/Controllers/DynamicHlsController.cs
  50. 3 1
      Jellyfin.Api/Controllers/HlsSegmentController.cs
  51. 8 8
      Jellyfin.Api/Controllers/ImageController.cs
  52. 82 8
      Jellyfin.Api/Controllers/InstantMixController.cs
  53. 2 1
      Jellyfin.Api/Controllers/ItemLookupController.cs
  54. 1 1
      Jellyfin.Api/Controllers/ItemUpdateController.cs
  55. 5 5
      Jellyfin.Api/Controllers/LibraryController.cs
  56. 5 8
      Jellyfin.Api/Controllers/LibraryStructureController.cs
  57. 7 14
      Jellyfin.Api/Controllers/NotificationsController.cs
  58. 10 10
      Jellyfin.Api/Controllers/PlaystateController.cs
  59. 1 1
      Jellyfin.Api/Controllers/PluginsController.cs
  60. 2 1
      Jellyfin.Api/Controllers/RemoteImageController.cs
  61. 1 1
      Jellyfin.Api/Controllers/UniversalAudioController.cs
  62. 4 4
      Jellyfin.Api/Controllers/VideoHlsController.cs
  63. 7 9
      Jellyfin.Api/Controllers/VideosController.cs
  64. 2 1
      Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
  65. 5 7
      Jellyfin.Api/Helpers/StreamingHelpers.cs
  66. 0 1
      Jellyfin.Api/Jellyfin.Api.csproj
  67. 6 9
      Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs
  68. 19 0
      Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoPathDto.cs
  69. 23 0
      Jellyfin.Api/Models/LibraryStructureDto/UpdateMediaPathRequestDto.cs
  70. 0 1
      Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
  71. 30 0
      Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs
  72. 7 53
      Jellyfin.Data/DayOfWeekHelper.cs
  73. 1 0
      Jellyfin.Data/Entities/Libraries/Collection.cs
  74. 2 0
      Jellyfin.Data/Entities/Libraries/MediaFileStream.cs
  75. 2 0
      Jellyfin.Data/Entities/Permission.cs
  76. 2 5
      Jellyfin.Data/Jellyfin.Data.csproj
  77. 7 5
      Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
  78. 4 9
      Jellyfin.Drawing.Skia/SkiaEncoder.cs
  79. 2 5
      Jellyfin.Networking/Jellyfin.Networking.csproj
  80. 16 5
      Jellyfin.Networking/Manager/NetworkManager.cs
  81. 4 4
      Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
  82. 7 7
      Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs
  83. 2 2
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs
  84. 3 3
      Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs
  85. 2 1
      Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
  86. 1 1
      Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
  87. 2 1
      Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
  88. 4 0
      Jellyfin.Server/CoreAppHost.cs
  89. 1 2
      Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
  90. 2 0
      Jellyfin.Server/Filters/WebsocketModelFilter.cs
  91. 1 1
      Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
  92. 1 1
      Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
  93. 3 6
      Jellyfin.Server/Jellyfin.Server.csproj
  94. 3 0
      Jellyfin.Server/Migrations/MigrationOptions.cs
  95. 1 1
      Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
  96. 6 5
      Jellyfin.Server/Program.cs
  97. 1 1
      Jellyfin.Server/Properties/AssemblyInfo.cs
  98. 2 2
      Jellyfin.Server/StartupOptions.cs
  99. 21 7
      Jellyfin.sln
  100. 89 0
      MediaBrowser.Common/Crc32.cs

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

@@ -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/net5.0/openapi.json"
+          targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net5.0/openapi.json"
           artifactName: 'OpenAPI Spec'
           artifactName: 'OpenAPI Spec'

+ 1 - 0
CONTRIBUTORS.md

@@ -49,6 +49,7 @@
  - [h1nk](https://github.com/h1nk)
  - [h1nk](https://github.com/h1nk)
  - [hawken93](https://github.com/hawken93)
  - [hawken93](https://github.com/hawken93)
  - [HelloWorld017](https://github.com/HelloWorld017)
  - [HelloWorld017](https://github.com/HelloWorld017)
+ - [ikomhoog](https://github.com/ikomhoog)
  - [jftuga](https://github.com/jftuga)
  - [jftuga](https://github.com/jftuga)
  - [joern-h](https://github.com/joern-h)
  - [joern-h](https://github.com/joern-h)
  - [joshuaboniface](https://github.com/joshuaboniface)
  - [joshuaboniface](https://github.com/joshuaboniface)

+ 9 - 3
Emby.Dlna/DlnaManager.cs

@@ -36,7 +36,7 @@ namespace Emby.Dlna
         private readonly ILogger<DlnaManager> _logger;
         private readonly ILogger<DlnaManager> _logger;
         private readonly IServerApplicationHost _appHost;
         private readonly IServerApplicationHost _appHost;
         private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
         private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
-        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 
 
         private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
         private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
 
 
@@ -333,7 +333,12 @@ namespace Emby.Dlna
                 throw new ArgumentNullException(nameof(id));
                 throw new ArgumentNullException(nameof(id));
             }
             }
 
 
-            var info = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
+            var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
+
+            if (info == null)
+            {
+                return null;
+            }
 
 
             return ParseProfileFile(info.Path, info.Info.Type);
             return ParseProfileFile(info.Path, info.Info.Type);
         }
         }
@@ -395,7 +400,8 @@ namespace Emby.Dlna
                     {
                     {
                         Directory.CreateDirectory(systemProfilesPath);
                         Directory.CreateDirectory(systemProfilesPath);
 
 
-                        using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
+                        // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+                        using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
                         {
                         {
                             await stream.CopyToAsync(fileStream).ConfigureAwait(false);
                             await stream.CopyToAsync(fileStream).ConfigureAwait(false);
                         }
                         }

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

@@ -25,7 +25,6 @@
 
 
   <!-- Code Analyzers-->
   <!-- Code Analyzers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />

+ 1 - 1
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -316,7 +316,7 @@ namespace Emby.Dlna.Main
                 _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
                 _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
 
 
                 var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
                 var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
-                if (_appHost.PublishedServerUrl == null)
+                if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl))
                 {
                 {
                     // DLNA will only work over http, so we must reset to http:// : {port}.
                     // DLNA will only work over http, so we must reset to http:// : {port}.
                     uri.Scheme = "http";
                     uri.Scheme = "http";

+ 1 - 1
Emby.Dlna/PlayTo/PlayToController.cs

@@ -132,7 +132,7 @@ namespace Emby.Dlna.PlayTo
 
 
         private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
         private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
         {
         {
-            if (_disposed)
+            if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
             {
             {
                 return;
                 return;
             }
             }

+ 8 - 8
Emby.Dlna/PlayTo/SsdpHttpClient.cs

@@ -45,10 +45,10 @@ namespace Emby.Dlna.PlayTo
                     cancellationToken)
                     cancellationToken)
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            using var reader = new StreamReader(stream, Encoding.UTF8);
-            return XDocument.Parse(
-                await reader.ReadToEndAsync().ConfigureAwait(false),
-                LoadOptions.PreserveWhitespace);
+            return await XDocument.LoadAsync(
+                stream,
+                LoadOptions.PreserveWhitespace,
+                cancellationToken).ConfigureAwait(false);
         }
         }
 
 
         private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
         private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
@@ -94,10 +94,10 @@ namespace Emby.Dlna.PlayTo
             options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
             options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
             using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
             using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            using var reader = new StreamReader(stream, Encoding.UTF8);
-            return XDocument.Parse(
-                await reader.ReadToEndAsync().ConfigureAwait(false),
-                LoadOptions.PreserveWhitespace);
+            return await XDocument.LoadAsync(
+                stream,
+                LoadOptions.PreserveWhitespace,
+                cancellationToken).ConfigureAwait(false);
         }
         }
 
 
         private async Task<HttpResponseMessage> PostSoapDataAsync(
         private async Task<HttpResponseMessage> PostSoapDataAsync(

+ 2 - 4
Emby.Dlna/PlayTo/TransportCommands.cs

@@ -13,12 +13,10 @@ namespace Emby.Dlna.PlayTo
     public class TransportCommands
     public class TransportCommands
     {
     {
         private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
         private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
-        private List<StateVariable> _stateVariables = new List<StateVariable>();
-        private List<ServiceAction> _serviceActions = new List<ServiceAction>();
 
 
-        public List<StateVariable> StateVariables => _stateVariables;
+        public List<StateVariable> StateVariables { get; } = new List<StateVariable>();
 
 
-        public List<ServiceAction> ServiceActions => _serviceActions;
+        public List<ServiceAction> ServiceActions { get; } = new List<ServiceAction>();
 
 
         public static TransportCommands Create(XDocument document)
         public static TransportCommands Create(XDocument document)
         {
         {

+ 3 - 0
Emby.Dlna/Profiles/DefaultProfile.cs

@@ -1,5 +1,7 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
+using System;
+using System.Globalization;
 using System.Linq;
 using System.Linq;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dlna;
 
 
@@ -10,6 +12,7 @@ namespace Emby.Dlna.Profiles
     {
     {
         public DefaultProfile()
         public DefaultProfile()
         {
         {
+            Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
             Name = "Generic Device";
             Name = "Generic Device";
 
 
             ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*";
             ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*";

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

@@ -25,7 +25,6 @@
 
 
   <!-- Code analysers-->
   <!-- Code analysers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />

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

@@ -44,7 +44,6 @@
 
 
   <!-- Code Analyzers-->
   <!-- Code Analyzers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <!-- TODO: <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> -->
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />

+ 9 - 2
Emby.Naming/Video/CleanStringParser.cs

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
 
 
 namespace Emby.Naming.Video
 namespace Emby.Naming.Video
@@ -16,8 +17,14 @@ namespace Emby.Naming.Video
         /// <param name="expressions">List of regex to parse name and year from.</param>
         /// <param name="expressions">List of regex to parse name and year from.</param>
         /// <param name="newName">Parsing result string.</param>
         /// <param name="newName">Parsing result string.</param>
         /// <returns>True if parsing was successful.</returns>
         /// <returns>True if parsing was successful.</returns>
-        public static bool TryClean(string name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName)
+        public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName)
         {
         {
+            if (string.IsNullOrEmpty(name))
+            {
+                newName = ReadOnlySpan<char>.Empty;
+                return false;
+            }
+
             var len = expressions.Count;
             var len = expressions.Count;
             for (int i = 0; i < len; i++)
             for (int i = 0; i < len; i++)
             {
             {
@@ -41,7 +48,7 @@ namespace Emby.Naming.Video
                 return true;
                 return true;
             }
             }
 
 
-            newName = string.Empty;
+            newName = ReadOnlySpan<char>.Empty;
             return false;
             return false;
         }
         }
     }
     }

+ 9 - 7
Emby.Naming/Video/VideoListResolver.cs

@@ -221,20 +221,22 @@ namespace Emby.Naming.Video
             string testFilename = Path.GetFileNameWithoutExtension(testFilePath);
             string testFilename = Path.GetFileNameWithoutExtension(testFilePath);
             if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
             if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
             {
             {
-                if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
+                // Remove the folder name before cleaning as we don't care about cleaning that part
+                if (folderName.Length <= testFilename.Length)
                 {
                 {
-                    testFilename = cleanName.ToString();
+                    testFilename = testFilename.Substring(folderName.Length).Trim();
                 }
                 }
 
 
-                if (folderName.Length <= testFilename.Length)
+                if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
                 {
                 {
-                    testFilename = testFilename.Substring(folderName.Length).Trim();
+                    testFilename = cleanName.Trim().ToString();
                 }
                 }
 
 
+                // The CleanStringParser should have removed common keywords etc.
                 return string.IsNullOrEmpty(testFilename)
                 return string.IsNullOrEmpty(testFilename)
-                   || testFilename[0] == '-'
-                   || testFilename[0] == '_'
-                   || string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
+                       || testFilename[0] == '-'
+                       || testFilename[0] == '_'
+                       || Regex.IsMatch(testFilename, @"^\[([^]]*)\]");
             }
             }
 
 
             return false;
             return false;

+ 2 - 1
Emby.Naming/Video/VideoResolver.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using Emby.Naming.Common;
 using Emby.Naming.Common;
@@ -146,7 +147,7 @@ namespace Emby.Naming.Video
         /// <param name="name">Raw name.</param>
         /// <param name="name">Raw name.</param>
         /// <param name="newName">Clean name.</param>
         /// <param name="newName">Clean name.</param>
         /// <returns>True if cleaning of name was successful.</returns>
         /// <returns>True if cleaning of name was successful.</returns>
-        public bool TryCleanString(string name, out ReadOnlySpan<char> newName)
+        public bool TryCleanString([NotNullWhen(true)] string? name, out ReadOnlySpan<char> newName)
         {
         {
             return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
             return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
         }
         }

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

@@ -25,7 +25,6 @@
 
 
   <!-- Code analyzers-->
   <!-- Code analyzers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />

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

@@ -28,7 +28,6 @@
 
 
   <!-- Code Analyzers-->
   <!-- Code Analyzers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />

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

@@ -53,7 +53,8 @@ namespace Emby.Server.Implementations.AppBase
 
 
                 Directory.CreateDirectory(directory);
                 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))
+                // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+                using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
                 {
                 {
                     fs.Write(newBytes, 0, newBytesLen);
                     fs.Write(newBytes, 0, newBytesLen);
                 }
                 }

+ 27 - 15
Emby.Server.Implementations/ApplicationHost.cs

@@ -10,8 +10,6 @@ using System.Net;
 using System.Reflection;
 using System.Reflection;
 using System.Runtime.InteropServices;
 using System.Runtime.InteropServices;
 using System.Security.Cryptography.X509Certificates;
 using System.Security.Cryptography.X509Certificates;
-using System.Text;
-using System.Text.Json;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Emby.Dlna;
 using Emby.Dlna;
@@ -43,6 +41,7 @@ using Emby.Server.Implementations.Serialization;
 using Emby.Server.Implementations.Session;
 using Emby.Server.Implementations.Session;
 using Emby.Server.Implementations.SyncPlay;
 using Emby.Server.Implementations.SyncPlay;
 using Emby.Server.Implementations.TV;
 using Emby.Server.Implementations.TV;
+using Emby.Server.Implementations.Udp;
 using Emby.Server.Implementations.Updates;
 using Emby.Server.Implementations.Updates;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Networking.Configuration;
 using Jellyfin.Networking.Configuration;
@@ -50,7 +49,6 @@ using Jellyfin.Networking.Manager;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Events;
 using MediaBrowser.Common.Events;
-using MediaBrowser.Common.Json;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Updates;
 using MediaBrowser.Common.Updates;
@@ -99,6 +97,7 @@ using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.XbmcMetadata.Providers;
 using MediaBrowser.XbmcMetadata.Providers;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using Prometheus.DotNetRuntime;
 using Prometheus.DotNetRuntime;
@@ -118,6 +117,7 @@ namespace Emby.Server.Implementations
         private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
         private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
 
 
         private readonly IFileSystem _fileSystemManager;
         private readonly IFileSystem _fileSystemManager;
+        private readonly IConfiguration _startupConfig;
         private readonly IXmlSerializer _xmlSerializer;
         private readonly IXmlSerializer _xmlSerializer;
         private readonly IStartupOptions _startupOptions;
         private readonly IStartupOptions _startupOptions;
         private readonly IPluginManager _pluginManager;
         private readonly IPluginManager _pluginManager;
@@ -126,7 +126,6 @@ namespace Emby.Server.Implementations
         private IMediaEncoder _mediaEncoder;
         private IMediaEncoder _mediaEncoder;
         private ISessionManager _sessionManager;
         private ISessionManager _sessionManager;
         private string[] _urlPrefixes;
         private string[] _urlPrefixes;
-        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
 
 
         /// <summary>
         /// <summary>
         /// Gets a value indicating whether this instance can self restart.
         /// Gets a value indicating whether this instance can self restart.
@@ -135,9 +134,6 @@ namespace Emby.Server.Implementations
 
 
         public bool CoreStartupHasCompleted { get; private set; }
         public bool CoreStartupHasCompleted { get; private set; }
 
 
-        /// <inheritdoc />
-        public Uri PublishedServerUrl => _startupOptions.PublishedServerUrl;
-
         public virtual bool CanLaunchWebBrowser
         public virtual bool CanLaunchWebBrowser
         {
         {
             get
             get
@@ -231,6 +227,11 @@ namespace Emby.Server.Implementations
         /// </summary>
         /// </summary>
         public int HttpsPort { get; private set; }
         public int HttpsPort { get; private set; }
 
 
+        /// <summary>
+        /// Gets the value of the PublishedServerUrl setting.
+        /// </summary>
+        public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
+
         /// <summary>
         /// <summary>
         /// Gets the server configuration manager.
         /// Gets the server configuration manager.
         /// </summary>
         /// </summary>
@@ -243,12 +244,14 @@ namespace Emby.Server.Implementations
         /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
         /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
         /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
         /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
         /// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
         /// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
+        /// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
         /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
         /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
         /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
         /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
         public ApplicationHost(
         public ApplicationHost(
             IServerApplicationPaths applicationPaths,
             IServerApplicationPaths applicationPaths,
             ILoggerFactory loggerFactory,
             ILoggerFactory loggerFactory,
             IStartupOptions options,
             IStartupOptions options,
+            IConfiguration startupConfig,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
             IServiceCollection serviceCollection)
             IServiceCollection serviceCollection)
         {
         {
@@ -271,6 +274,7 @@ namespace Emby.Server.Implementations
             Logger = LoggerFactory.CreateLogger<ApplicationHost>();
             Logger = LoggerFactory.CreateLogger<ApplicationHost>();
 
 
             _startupOptions = options;
             _startupOptions = options;
+            _startupConfig = startupConfig;
 
 
             // Initialize runtime stat collection
             // Initialize runtime stat collection
             if (ServerConfigurationManager.Configuration.EnableMetrics)
             if (ServerConfigurationManager.Configuration.EnableMetrics)
@@ -463,7 +467,7 @@ namespace Emby.Server.Implementations
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public IReadOnlyCollection<T> GetExports<T>(CreationDelegate defaultFunc, bool manageLifetime = true)
+        public IReadOnlyCollection<T> GetExports<T>(CreationDelegateFactory defaultFunc, bool manageLifetime = true)
         {
         {
             // Convert to list so this isn't executed for each iteration
             // Convert to list so this isn't executed for each iteration
             var parts = GetExportTypes<T>()
             var parts = GetExportTypes<T>()
@@ -487,8 +491,9 @@ namespace Emby.Server.Implementations
         /// Runs the startup tasks.
         /// Runs the startup tasks.
         /// </summary>
         /// </summary>
         /// <returns><see cref="Task" />.</returns>
         /// <returns><see cref="Task" />.</returns>
-        public async Task RunStartupTasksAsync()
+        public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
         {
         {
+            cancellationToken.ThrowIfCancellationRequested();
             Logger.LogInformation("Running startup tasks");
             Logger.LogInformation("Running startup tasks");
 
 
             Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
             Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
@@ -502,14 +507,21 @@ namespace Emby.Server.Implementations
 
 
             var entryPoints = GetExports<IServerEntryPoint>();
             var entryPoints = GetExports<IServerEntryPoint>();
 
 
+            cancellationToken.ThrowIfCancellationRequested();
+
             var stopWatch = new Stopwatch();
             var stopWatch = new Stopwatch();
             stopWatch.Start();
             stopWatch.Start();
+
             await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false);
             await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false);
             Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
             Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
 
 
             Logger.LogInformation("Core startup complete");
             Logger.LogInformation("Core startup complete");
             CoreStartupHasCompleted = true;
             CoreStartupHasCompleted = true;
+
+            cancellationToken.ThrowIfCancellationRequested();
+
             stopWatch.Restart();
             stopWatch.Restart();
+
             await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
             await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
             Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
             Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
             stopWatch.Stop();
             stopWatch.Stop();
@@ -1148,10 +1160,10 @@ namespace Emby.Server.Implementations
         public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
         public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
         {
         {
             // Published server ends with a /
             // Published server ends with a /
-            if (_startupOptions.PublishedServerUrl != null)
+            if (!string.IsNullOrEmpty(PublishedServerUrl))
             {
             {
                 // Published server ends with a '/', so we need to remove it.
                 // Published server ends with a '/', so we need to remove it.
-                return _startupOptions.PublishedServerUrl.ToString().Trim('/');
+                return PublishedServerUrl.Trim('/');
             }
             }
 
 
             string smart = NetManager.GetBindInterface(ipAddress, out port);
             string smart = NetManager.GetBindInterface(ipAddress, out port);
@@ -1168,10 +1180,10 @@ namespace Emby.Server.Implementations
         public string GetSmartApiUrl(HttpRequest request, int? port = null)
         public string GetSmartApiUrl(HttpRequest request, int? port = null)
         {
         {
             // Published server ends with a /
             // Published server ends with a /
-            if (_startupOptions.PublishedServerUrl != null)
+            if (!string.IsNullOrEmpty(PublishedServerUrl))
             {
             {
                 // Published server ends with a '/', so we need to remove it.
                 // Published server ends with a '/', so we need to remove it.
-                return _startupOptions.PublishedServerUrl.ToString().Trim('/');
+                return PublishedServerUrl.Trim('/');
             }
             }
 
 
             string smart = NetManager.GetBindInterface(request, out port);
             string smart = NetManager.GetBindInterface(request, out port);
@@ -1188,10 +1200,10 @@ namespace Emby.Server.Implementations
         public string GetSmartApiUrl(string hostname, int? port = null)
         public string GetSmartApiUrl(string hostname, int? port = null)
         {
         {
             // Published server ends with a /
             // Published server ends with a /
-            if (_startupOptions.PublishedServerUrl != null)
+            if (!string.IsNullOrEmpty(PublishedServerUrl))
             {
             {
                 // Published server ends with a '/', so we need to remove it.
                 // Published server ends with a '/', so we need to remove it.
-                return _startupOptions.PublishedServerUrl.ToString().Trim('/');
+                return PublishedServerUrl.Trim('/');
             }
             }
 
 
             string smart = NetManager.GetBindInterface(hostname, out port);
             string smart = NetManager.GetBindInterface(hostname, out port);

+ 1 - 1
Emby.Server.Implementations/Channels/ChannelManager.cs

@@ -49,7 +49,7 @@ namespace Emby.Server.Implementations.Channels
         private readonly IProviderManager _providerManager;
         private readonly IProviderManager _providerManager;
         private readonly IMemoryCache _memoryCache;
         private readonly IMemoryCache _memoryCache;
         private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
         private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
-        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="ChannelManager"/> class.
         /// Initializes a new instance of the <see cref="ChannelManager"/> class.

+ 14 - 1
Emby.Server.Implementations/Collections/CollectionManager.cs

@@ -344,7 +344,20 @@ namespace Emby.Server.Implementations.Collections
                     }
                     }
                     else
                     else
                     {
                     {
-                        results[item.Id] = item;
+                        var alreadyInResults = false;
+                        foreach (var child in item.GetMediaSources(true))
+                        {
+                            if (Guid.TryParse(child.Id, out var id) && results.ContainsKey(id))
+                            {
+                                alreadyInResults = true;
+                                break;
+                            }
+                        }
+
+                        if (!alreadyInResults)
+                        {
+                            results[item.Id] = item;
+                        }
                     }
                     }
                 }
                 }
             }
             }

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

@@ -88,7 +88,7 @@ namespace Emby.Server.Implementations.Data
             _imageProcessor = imageProcessor;
             _imageProcessor = imageProcessor;
 
 
             _typeMapper = new TypeMapper();
             _typeMapper = new TypeMapper();
-            _jsonOptions = JsonDefaults.GetOptions();
+            _jsonOptions = JsonDefaults.Options;
 
 
             DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db");
             DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db");
         }
         }

+ 0 - 1
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -50,7 +50,6 @@
 
 
   <!-- Code Analyzers-->
   <!-- Code Analyzers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />

+ 11 - 0
Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs

@@ -1,5 +1,6 @@
 #nullable enable
 #nullable enable
 
 
+using System;
 using System.Net.Sockets;
 using System.Net.Sockets;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
@@ -51,6 +52,8 @@ namespace Emby.Server.Implementations.EntryPoints
         /// <inheritdoc />
         /// <inheritdoc />
         public Task RunAsync()
         public Task RunAsync()
         {
         {
+            CheckDisposed();
+
             try
             try
             {
             {
                 _udpServer = new UdpServer(_logger, _appHost, _config);
                 _udpServer = new UdpServer(_logger, _appHost, _config);
@@ -64,6 +67,14 @@ namespace Emby.Server.Implementations.EntryPoints
             return Task.CompletedTask;
             return Task.CompletedTask;
         }
         }
 
 
+        private void CheckDisposed()
+        {
+            if (_disposed)
+            {
+                throw new ObjectDisposedException(this.GetType().Name);
+            }
+        }
+
         /// <inheritdoc />
         /// <inheritdoc />
         public void Dispose()
         public void Dispose()
         {
         {

+ 1 - 1
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -56,7 +56,7 @@ namespace Emby.Server.Implementations.HttpServer
             RemoteEndPoint = remoteEndPoint;
             RemoteEndPoint = remoteEndPoint;
             QueryString = query;
             QueryString = query;
 
 
-            _jsonOptions = JsonDefaults.GetOptions();
+            _jsonOptions = JsonDefaults.Options;
             LastActivityDate = DateTime.Now;
             LastActivityDate = DateTime.Now;
         }
         }
 
 

+ 6 - 6
Emby.Server.Implementations/IStartupOptions.cs

@@ -1,5 +1,5 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-
+#nullable enable
 using System;
 using System;
 
 
 namespace Emby.Server.Implementations
 namespace Emby.Server.Implementations
@@ -9,7 +9,7 @@ namespace Emby.Server.Implementations
         /// <summary>
         /// <summary>
         /// Gets the value of the --ffmpeg command line option.
         /// Gets the value of the --ffmpeg command line option.
         /// </summary>
         /// </summary>
-        string FFmpegPath { get; }
+        string? FFmpegPath { get; }
 
 
         /// <summary>
         /// <summary>
         /// Gets the value of the --service command line option.
         /// Gets the value of the --service command line option.
@@ -19,21 +19,21 @@ namespace Emby.Server.Implementations
         /// <summary>
         /// <summary>
         /// Gets the value of the --package-name command line option.
         /// Gets the value of the --package-name command line option.
         /// </summary>
         /// </summary>
-        string PackageName { get; }
+        string? PackageName { get; }
 
 
         /// <summary>
         /// <summary>
         /// Gets the value of the --restartpath command line option.
         /// Gets the value of the --restartpath command line option.
         /// </summary>
         /// </summary>
-        string RestartPath { get; }
+        string? RestartPath { get; }
 
 
         /// <summary>
         /// <summary>
         /// Gets the value of the --restartargs command line option.
         /// Gets the value of the --restartargs command line option.
         /// </summary>
         /// </summary>
-        string RestartArgs { get; }
+        string? RestartArgs { get; }
 
 
         /// <summary>
         /// <summary>
         /// Gets the value of the --published-server-url command line option.
         /// Gets the value of the --published-server-url command line option.
         /// </summary>
         /// </summary>
-        Uri PublishedServerUrl { get; }
+        string? PublishedServerUrl { get; }
     }
     }
 }
 }

+ 12 - 60
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -1247,7 +1247,7 @@ namespace Emby.Server.Implementations.Library
             {
             {
                 // TODO: @bond use a ReadOnlySpan<char> here when Enum.TryParse supports it
                 // TODO: @bond use a ReadOnlySpan<char> here when Enum.TryParse supports it
                 // https://github.com/dotnet/runtime/issues/20008
                 // https://github.com/dotnet/runtime/issues/20008
-                if (Enum.TryParse<CollectionTypeOptions>(Path.GetExtension(file), true, out var res))
+                if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
                 {
                 {
                     return res;
                     return res;
                 }
                 }
@@ -2776,6 +2776,7 @@ namespace Emby.Server.Implementations.Library
 
 
         public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
         public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
         {
         {
+            string newPath;
             if (ownerItem != null)
             if (ownerItem != null)
             {
             {
                 var libraryOptions = GetLibraryOptions(ownerItem);
                 var libraryOptions = GetLibraryOptions(ownerItem);
@@ -2783,15 +2784,9 @@ namespace Emby.Server.Implementations.Library
                 {
                 {
                     foreach (var pathInfo in libraryOptions.PathInfos)
                     foreach (var pathInfo in libraryOptions.PathInfos)
                     {
                     {
-                        if (string.IsNullOrWhiteSpace(pathInfo.Path) || string.IsNullOrWhiteSpace(pathInfo.NetworkPath))
+                        if (path.TryReplaceSubPath(pathInfo.Path, pathInfo.NetworkPath, out newPath))
                         {
                         {
-                            continue;
-                        }
-
-                        var substitutionResult = SubstitutePathInternal(path, pathInfo.Path, pathInfo.NetworkPath);
-                        if (substitutionResult.Item2)
-                        {
-                            return substitutionResult.Item1;
+                            return newPath;
                         }
                         }
                     }
                     }
                 }
                 }
@@ -2800,24 +2795,16 @@ namespace Emby.Server.Implementations.Library
             var metadataPath = _configurationManager.Configuration.MetadataPath;
             var metadataPath = _configurationManager.Configuration.MetadataPath;
             var metadataNetworkPath = _configurationManager.Configuration.MetadataNetworkPath;
             var metadataNetworkPath = _configurationManager.Configuration.MetadataNetworkPath;
 
 
-            if (!string.IsNullOrWhiteSpace(metadataPath) && !string.IsNullOrWhiteSpace(metadataNetworkPath))
+            if (path.TryReplaceSubPath(metadataPath, metadataNetworkPath, out newPath))
             {
             {
-                var metadataSubstitutionResult = SubstitutePathInternal(path, metadataPath, metadataNetworkPath);
-                if (metadataSubstitutionResult.Item2)
-                {
-                    return metadataSubstitutionResult.Item1;
-                }
+                return newPath;
             }
             }
 
 
             foreach (var map in _configurationManager.Configuration.PathSubstitutions)
             foreach (var map in _configurationManager.Configuration.PathSubstitutions)
             {
             {
-                if (!string.IsNullOrWhiteSpace(map.From))
+                if (path.TryReplaceSubPath(map.From, map.To, out newPath))
                 {
                 {
-                    var substitutionResult = SubstitutePathInternal(path, map.From, map.To);
-                    if (substitutionResult.Item2)
-                    {
-                        return substitutionResult.Item1;
-                    }
+                    return newPath;
                 }
                 }
             }
             }
 
 
@@ -2826,47 +2813,12 @@ namespace Emby.Server.Implementations.Library
 
 
         public string SubstitutePath(string path, string from, string to)
         public string SubstitutePath(string path, string from, string to)
         {
         {
-            return SubstitutePathInternal(path, from, to).Item1;
-        }
-
-        private Tuple<string, bool> SubstitutePathInternal(string path, string from, string to)
-        {
-            if (string.IsNullOrWhiteSpace(path))
-            {
-                throw new ArgumentNullException(nameof(path));
-            }
-
-            if (string.IsNullOrWhiteSpace(from))
+            if (path.TryReplaceSubPath(from, to, out var newPath))
             {
             {
-                throw new ArgumentNullException(nameof(from));
+                return newPath;
             }
             }
 
 
-            if (string.IsNullOrWhiteSpace(to))
-            {
-                throw new ArgumentNullException(nameof(to));
-            }
-
-            from = from.Trim();
-            to = to.Trim();
-
-            var newPath = path.Replace(from, to, StringComparison.OrdinalIgnoreCase);
-            var changed = false;
-
-            if (!string.Equals(newPath, path, StringComparison.Ordinal))
-            {
-                if (to.IndexOf('/', StringComparison.Ordinal) != -1)
-                {
-                    newPath = newPath.Replace('\\', '/');
-                }
-                else
-                {
-                    newPath = newPath.Replace('/', '\\');
-                }
-
-                changed = true;
-            }
-
-            return new Tuple<string, bool>(newPath, changed);
+            return path;
         }
         }
 
 
         private void SetExtraTypeFromFilename(Video item)
         private void SetExtraTypeFromFilename(Video item)
@@ -3001,7 +2953,7 @@ namespace Emby.Server.Implementations.Library
 
 
                 if (collectionType != null)
                 if (collectionType != null)
                 {
                 {
-                    var path = Path.Combine(virtualFolderPath, collectionType.ToString() + ".collection");
+                    var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
 
 
                     File.WriteAllBytes(path, Array.Empty<byte>());
                     File.WriteAllBytes(path, Array.Empty<byte>());
                 }
                 }

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

@@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Library
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private readonly IApplicationPaths _appPaths;
         private readonly IApplicationPaths _appPaths;
-        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 
 
         public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IApplicationPaths appPaths)
         public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IApplicationPaths appPaths)
         {
         {

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

@@ -46,7 +46,7 @@ namespace Emby.Server.Implementations.Library
 
 
         private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
         private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
         private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
         private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
-        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 
 
         private IMediaSourceProvider[] _providers;
         private IMediaSourceProvider[] _providers;
 
 

+ 63 - 0
Emby.Server.Implementations/Library/PathExtensions.cs

@@ -1,6 +1,8 @@
 #nullable enable
 #nullable enable
 
 
 using System;
 using System;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
 
 
 namespace Emby.Server.Implementations.Library
 namespace Emby.Server.Implementations.Library
@@ -47,5 +49,66 @@ namespace Emby.Server.Implementations.Library
 
 
             return null;
             return null;
         }
         }
+
+        /// <summary>
+        /// Replaces a sub path with another sub path and normalizes the final path.
+        /// </summary>
+        /// <param name="path">The original path.</param>
+        /// <param name="subPath">The original sub path.</param>
+        /// <param name="newSubPath">The new sub path.</param>
+        /// <param name="newPath">The result of the sub path replacement</param>
+        /// <returns>The path after replacing the sub path.</returns>
+        /// <exception cref="ArgumentNullException"><paramref name="path" />, <paramref name="newSubPath" /> or <paramref name="newSubPath" /> is empty.</exception>
+        public static bool TryReplaceSubPath(
+            [NotNullWhen(true)] this string? path,
+            [NotNullWhen(true)] string? subPath,
+            [NotNullWhen(true)] string? newSubPath,
+            [NotNullWhen(true)] out string? newPath)
+        {
+            newPath = null;
+
+            if (string.IsNullOrEmpty(path)
+                || string.IsNullOrEmpty(subPath)
+                || string.IsNullOrEmpty(newSubPath)
+                || subPath.Length > path.Length)
+            {
+                return false;
+            }
+
+            char oldDirectorySeparatorChar;
+            char newDirectorySeparatorChar;
+            // True normalization is still not possible https://github.com/dotnet/runtime/issues/2162
+            // The reasoning behind this is that a forward slash likely means it's a Linux path and
+            // so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much).
+            if (newSubPath.Contains('/', StringComparison.Ordinal))
+            {
+                oldDirectorySeparatorChar = '\\';
+                newDirectorySeparatorChar = '/';
+            }
+            else
+            {
+                oldDirectorySeparatorChar = '/';
+                newDirectorySeparatorChar = '\\';
+            }
+
+            path = path.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
+            subPath = subPath.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
+
+            // We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results
+            // when the sub path matches a similar but in-complete subpath
+            var oldSubPathEndsWithSeparator = subPath[^1] == newDirectorySeparatorChar;
+            if (!path.StartsWith(subPath, StringComparison.OrdinalIgnoreCase)
+                || (!oldSubPathEndsWithSeparator && path[subPath.Length] != newDirectorySeparatorChar))
+            {
+                return false;
+            }
+
+            var newSubPathTrimmed = newSubPath.AsSpan().TrimEnd(newDirectorySeparatorChar);
+            // Ensure that the path with the old subpath removed starts with a leading dir separator
+            int idx = oldSubPathEndsWithSeparator ? subPath.Length - 1 : subPath.Length;
+            newPath = string.Concat(newSubPathTrimmed, path.AsSpan(idx));
+
+            return true;
+        }
     }
     }
 }
 }

+ 4 - 2
Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs

@@ -45,7 +45,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         {
         {
             Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
             Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
 
 
-            using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read))
+            // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+            using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None))
             {
             {
                 onStarted();
                 onStarted();
 
 
@@ -70,7 +71,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
 
             Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
             Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
 
 
-            await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read);
+            // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+            await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None);
 
 
             onStarted();
             onStarted();
 
 

+ 4 - 2
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -1856,7 +1856,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 return;
                 return;
             }
             }
 
 
-            using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
+            // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+            using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
             {
             {
                 var settings = new XmlWriterSettings
                 var settings = new XmlWriterSettings
                 {
                 {
@@ -1920,7 +1921,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 return;
                 return;
             }
             }
 
 
-            using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
+            // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+            using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
             {
             {
                 var settings = new XmlWriterSettings
                 var settings = new XmlWriterSettings
                 {
                 {

+ 1 - 1
Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         private readonly IServerApplicationPaths _appPaths;
         private readonly IServerApplicationPaths _appPaths;
         private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
         private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
         private readonly IServerConfigurationManager _serverConfigurationManager;
         private readonly IServerConfigurationManager _serverConfigurationManager;
-        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
         private bool _hasExited;
         private bool _hasExited;
         private Stream _logFileStream;
         private Stream _logFileStream;
         private string _targetPath;
         private string _targetPath;

+ 1 - 1
Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs

@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
     {
     {
         private readonly string _dataPath;
         private readonly string _dataPath;
         private readonly object _fileDataLock = new object();
         private readonly object _fileDataLock = new object();
-        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
         private T[] _items;
         private T[] _items;
 
 
         public ItemDataProvider(
         public ItemDataProvider(

+ 1 - 1
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
         private readonly ICryptoProvider _cryptoProvider;
         private readonly ICryptoProvider _cryptoProvider;
 
 
         private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
         private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
-        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
         private DateTime _lastErrorResponse;
         private DateTime _lastErrorResponse;
 
 
         public SchedulesDirect(
         public SchedulesDirect(

+ 1 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs

@@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             _networkManager = networkManager;
             _networkManager = networkManager;
             _streamHelper = streamHelper;
             _streamHelper = streamHelper;
 
 
-            _jsonOptions = JsonDefaults.GetOptions();
+            _jsonOptions = JsonDefaults.Options;
         }
         }
 
 
         public string Name => "HD Homerun";
         public string Name => "HD Homerun";

+ 70 - 186
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs

@@ -2,6 +2,7 @@
 
 
 using System;
 using System;
 using System.Buffers;
 using System.Buffers;
+using System.Buffers.Binary;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Net;
 using System.Net;
@@ -10,6 +11,7 @@ using System.Text;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using MediaBrowser.Common;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
 
 
 namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
@@ -120,13 +122,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 
 
         private static async Task<bool> CheckTunerAvailability(NetworkStream stream, int tuner, CancellationToken cancellationToken)
         private static async Task<bool> CheckTunerAvailability(NetworkStream stream, int tuner, CancellationToken cancellationToken)
         {
         {
-            var lockkeyMsg = CreateGetMessage(tuner, "lockkey");
-            await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
-
             byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
             byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
             try
             try
             {
             {
-                int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+                var msgLen = WriteGetMessage(buffer, tuner, "lockkey");
+                await stream.WriteAsync(buffer.AsMemory(0, msgLen), cancellationToken).ConfigureAwait(false);
+
+                int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
 
 
                 ParseReturnMessage(buffer, receivedBytes, out string returnVal);
                 ParseReturnMessage(buffer, receivedBytes, out string returnVal);
 
 
@@ -166,9 +168,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 
 
                     _activeTuner = i;
                     _activeTuner = i;
                     var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue);
                     var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue);
-                    var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null);
-                    await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
-                    int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+                    var lockkeyMsgLen = WriteSetMessage(buffer, i, "lockkey", lockKeyString, null);
+                    await stream.WriteAsync(buffer.AsMemory(0, lockkeyMsgLen), cancellationToken).ConfigureAwait(false);
+                    int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
 
 
                     // parse response to make sure it worked
                     // parse response to make sure it worked
                     if (!ParseReturnMessage(buffer, receivedBytes, out _))
                     if (!ParseReturnMessage(buffer, receivedBytes, out _))
@@ -178,9 +180,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 
 
                     foreach (var command in commands.GetCommands())
                     foreach (var command in commands.GetCommands())
                     {
                     {
-                        var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue);
-                        await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
-                        receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+                        var channelMsgLen = WriteSetMessage(buffer, i, command.Item1, command.Item2, lockKeyValue);
+                        await stream.WriteAsync(buffer.AsMemory(0, channelMsgLen), cancellationToken).ConfigureAwait(false);
+                        receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
 
 
                         // parse response to make sure it worked
                         // parse response to make sure it worked
                         if (!ParseReturnMessage(buffer, receivedBytes, out _))
                         if (!ParseReturnMessage(buffer, receivedBytes, out _))
@@ -191,10 +193,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                     }
                     }
 
 
                     var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort);
                     var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort);
-                    var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue);
+                    var targetMsgLen = WriteSetMessage(buffer, i, "target", targetValue, lockKeyValue);
 
 
-                    await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false);
-                    receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+                    await stream.WriteAsync(buffer.AsMemory(0, targetMsgLen), cancellationToken).ConfigureAwait(false);
+                    receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
 
 
                     // parse response to make sure it worked
                     // parse response to make sure it worked
                     if (!ParseReturnMessage(buffer, receivedBytes, out _))
                     if (!ParseReturnMessage(buffer, receivedBytes, out _))
@@ -232,9 +234,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             {
             {
                 foreach (var command in commandList)
                 foreach (var command in commandList)
                 {
                 {
-                    var channelMsg = CreateSetMessage(_activeTuner, command.Item1, command.Item2, _lockkey);
-                    await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
-                    int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+                    var channelMsgLen = WriteSetMessage(buffer, _activeTuner, command.Item1, command.Item2, _lockkey);
+                    await stream.WriteAsync(buffer.AsMemory(0, channelMsgLen), cancellationToken).ConfigureAwait(false);
+                    int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
 
 
                     // parse response to make sure it worked
                     // parse response to make sure it worked
                     if (!ParseReturnMessage(buffer, receivedBytes, out _))
                     if (!ParseReturnMessage(buffer, receivedBytes, out _))
@@ -265,17 +267,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
         {
         {
             var stream = client.GetStream();
             var stream = client.GetStream();
 
 
-            var releaseTarget = CreateSetMessage(_activeTuner, "target", "none", lockKeyValue);
-            await stream.WriteAsync(releaseTarget, 0, releaseTarget.Length).ConfigureAwait(false);
-
             var buffer = ArrayPool<byte>.Shared.Rent(8192);
             var buffer = ArrayPool<byte>.Shared.Rent(8192);
             try
             try
             {
             {
-                await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
-                var releaseKeyMsg = CreateSetMessage(_activeTuner, "lockkey", "none", lockKeyValue);
+                var releaseTargetLen = WriteSetMessage(buffer, _activeTuner, "target", "none", lockKeyValue);
+                await stream.WriteAsync(buffer.AsMemory(0, releaseTargetLen)).ConfigureAwait(false);
+
+                await stream.ReadAsync(buffer).ConfigureAwait(false);
+                var releaseKeyMsgLen = WriteSetMessage(buffer, _activeTuner, "lockkey", "none", lockKeyValue);
                 _lockkey = null;
                 _lockkey = null;
-                await stream.WriteAsync(releaseKeyMsg, 0, releaseKeyMsg.Length).ConfigureAwait(false);
-                await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
+                await stream.WriteAsync(buffer.AsMemory(0, releaseKeyMsgLen)).ConfigureAwait(false);
+                await stream.ReadAsync(buffer).ConfigureAwait(false);
             }
             }
             finally
             finally
             {
             {
@@ -283,107 +285,74 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             }
             }
         }
         }
 
 
-        private static byte[] CreateGetMessage(int tuner, string name)
+        internal static int WriteGetMessage(Span<byte> buffer, int tuner, string name)
         {
         {
-            var byteName = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}\0", tuner, name));
-            int messageLength = byteName.Length + 10; // 4 bytes for header + 4 bytes for crc + 2 bytes for tag name and length
-
-            var message = new byte[messageLength];
+            var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name);
+            int offset = WriteHeaderAndPayload(buffer, byteName);
+            return FinishPacket(buffer, offset);
+        }
 
 
-            int offset = InsertHeaderAndName(byteName, messageLength, message);
+        private static int WriteSetMessage(Span<byte> buffer, int tuner, string name, string value, uint? lockkey)
+        {
+            var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name);
+            int offset = WriteHeaderAndPayload(buffer, byteName);
 
 
-            bool flipEndian = BitConverter.IsLittleEndian;
+            buffer[offset++] = GetSetValue;
+            offset += WriteNullTerminatedString(buffer.Slice(offset), value);
 
 
-            // calculate crc and insert at the end of the message
-            var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4));
-            if (flipEndian)
+            if (lockkey.HasValue)
             {
             {
-                Array.Reverse(crcBytes);
+                buffer[offset++] = GetSetLockkey;
+                buffer[offset++] = 4;
+                BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(offset), lockkey.Value);
+                offset += 4;
             }
             }
 
 
-            Buffer.BlockCopy(crcBytes, 0, message, offset, 4);
-
-            return message;
+            return FinishPacket(buffer, offset);
         }
         }
 
 
-        private static byte[] CreateSetMessage(int tuner, string name, string value, uint? lockkey)
+        internal static int WriteNullTerminatedString(Span<byte> buffer, ReadOnlySpan<char> payload)
         {
         {
-            var byteName = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}\0", tuner, name));
-            var byteValue = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "{0}\0", value));
+            int len = Encoding.UTF8.GetBytes(payload, buffer.Slice(1)) + 1;
 
 
-            int messageLength = byteName.Length + byteValue.Length + 12;
-            if (lockkey.HasValue)
-            {
-                messageLength += 6;
-            }
+            // TODO: variable length: this can be 2 bytes if len > 127
+            // Write length in front of value
+            buffer[0] = Convert.ToByte(len);
 
 
-            var message = new byte[messageLength];
+            // null-terminate
+            buffer[len++] = 0;
 
 
-            int offset = InsertHeaderAndName(byteName, messageLength, message);
+            return len;
+        }
 
 
-            bool flipEndian = BitConverter.IsLittleEndian;
+        private static int WriteHeaderAndPayload(Span<byte> buffer, ReadOnlySpan<char> payload)
+        {
+            // Packet type
+            BinaryPrimitives.WriteUInt16BigEndian(buffer, GetSetRequest);
 
 
-            message[offset++] = GetSetValue;
-            message[offset++] = Convert.ToByte(byteValue.Length);
-            Buffer.BlockCopy(byteValue, 0, message, offset, byteValue.Length);
-            offset += byteValue.Length;
-            if (lockkey.HasValue)
-            {
-                message[offset++] = GetSetLockkey;
-                message[offset++] = 4;
-                var lockKeyBytes = BitConverter.GetBytes(lockkey.Value);
-                if (flipEndian)
-                {
-                    Array.Reverse(lockKeyBytes);
-                }
+            // We write the payload length at the end
+            int offset = 4;
 
 
-                Buffer.BlockCopy(lockKeyBytes, 0, message, offset, 4);
-                offset += 4;
-            }
+            // Tag
+            buffer[offset++] = GetSetName;
 
 
-            // calculate crc and insert at the end of the message
-            var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4));
-            if (flipEndian)
-            {
-                Array.Reverse(crcBytes);
-            }
-
-            Buffer.BlockCopy(crcBytes, 0, message, offset, 4);
+            // Payload length + data
+            int strLen = WriteNullTerminatedString(buffer.Slice(offset), payload);
+            offset += strLen;
 
 
-            return message;
+            return offset;
         }
         }
 
 
-        private static int InsertHeaderAndName(byte[] byteName, int messageLength, byte[] message)
+        private static int FinishPacket(Span<byte> buffer, int offset)
         {
         {
-            // check to see if we need to flip endiannes
-            bool flipEndian = BitConverter.IsLittleEndian;
-            int offset = 0;
-
-            // create header bytes
-            var getSetBytes = BitConverter.GetBytes(GetSetRequest);
-            var msgLenBytes = BitConverter.GetBytes((ushort)(messageLength - 8)); // Subtrace 4 bytes for header and 4 bytes for crc
-
-            if (flipEndian)
-            {
-                Array.Reverse(getSetBytes);
-                Array.Reverse(msgLenBytes);
-            }
-
-            // insert header bytes into message
-            Buffer.BlockCopy(getSetBytes, 0, message, offset, 2);
-            offset += 2;
-            Buffer.BlockCopy(msgLenBytes, 0, message, offset, 2);
-            offset += 2;
-
-            // insert tag name and length
-            message[offset++] = GetSetName;
-            message[offset++] = Convert.ToByte(byteName.Length);
+            // Payload length
+            BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), (ushort)(offset - 4));
 
 
-            // insert name string
-            Buffer.BlockCopy(byteName, 0, message, offset, byteName.Length);
-            offset += byteName.Length;
+            // calculate crc and insert at the end of the message
+            var crc = Crc32.Compute(buffer.Slice(0, offset));
+            BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset), crc);
 
 
-            return offset;
+            return offset + 4;
         }
         }
 
 
         private static bool ParseReturnMessage(byte[] buf, int numBytes, out string returnVal)
         private static bool ParseReturnMessage(byte[] buf, int numBytes, out string returnVal)
@@ -442,90 +411,5 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             returnVal = Encoding.UTF8.GetString(buf, offset, valueLength - 1); // remove null terminator
             returnVal = Encoding.UTF8.GetString(buf, offset, valueLength - 1); // remove null terminator
             return true;
             return true;
         }
         }
-
-        private static class HdHomerunCrc
-        {
-            private static uint[] crc_table = {
-            0x00000000, 0x77073096, 0xee0e612c, 0x990951ba,
-            0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
-            0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
-            0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
-            0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de,
-            0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
-            0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
-            0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
-            0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
-            0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
-            0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940,
-            0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
-            0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116,
-            0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
-            0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
-            0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
-            0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a,
-            0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
-            0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818,
-            0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
-            0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
-            0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
-            0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c,
-            0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
-            0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2,
-            0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
-            0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
-            0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
-            0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086,
-            0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
-            0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4,
-            0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
-            0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
-            0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
-            0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
-            0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
-            0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe,
-            0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
-            0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
-            0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
-            0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252,
-            0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
-            0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60,
-            0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
-            0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
-            0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
-            0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04,
-            0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
-            0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
-            0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
-            0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
-            0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
-            0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e,
-            0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
-            0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c,
-            0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
-            0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
-            0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
-            0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0,
-            0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
-            0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6,
-            0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
-            0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
-            0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d };
-
-            public static uint GetCrc32(byte[] bytes, int numBytes)
-            {
-                var hash = 0xffffffff;
-                for (var i = 0; i < numBytes; i++)
-                {
-                    hash = (hash >> 8) ^ crc_table[(hash ^ bytes[i]) & 0xff];
-                }
-
-                var tmp = ~hash & 0xffffffff;
-                var b0 = tmp & 0xff;
-                var b1 = (tmp >> 8) & 0xff;
-                var b2 = (tmp >> 16) & 0xff;
-                var b3 = (tmp >> 24) & 0xff;
-                return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
-            }
-        }
     }
     }
 }
 }

+ 23 - 1
Emby.Server.Implementations/Localization/Core/gl.json

@@ -57,5 +57,27 @@
     "DeviceOnlineWithName": "{0} conectouse",
     "DeviceOnlineWithName": "{0} conectouse",
     "DeviceOfflineWithName": "{0} desconectouse",
     "DeviceOfflineWithName": "{0} desconectouse",
     "Default": "Por defecto",
     "Default": "Por defecto",
-    "AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}"
+    "AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
+    "TaskCleanLogs": "Limpar Carpeta de Rexistros",
+    "TaskCleanActivityLog": "Limpar Rexistro de Actividade",
+    "TasksChannelsCategory": "Canáis de Internet",
+    "TaskUpdatePlugins": "Actualizar Plugins",
+    "User": "Usuario",
+    "Undefined": "Sen definir",
+    "TvShows": "Programas de TV",
+    "System": "Sistema",
+    "Sync": "Sincronizar",
+    "SubtitleDownloadFailureFromForItem": "Fallou a descarga de subtítulos para {1} dende {0}",
+    "StartupEmbyServerIsLoading": "O Servidor Jellyfin está cargando. Por favor, reinténteo en breve.",
+    "Songs": "Cancións",
+    "Shows": "Programas",
+    "ServerNameNeedsToBeRestarted": "{0} precisa ser reiniciado",
+    "ScheduledTaskStartedWithName": "{0} comezou",
+    "ScheduledTaskFailedWithName": "{0} fallou",
+    "ProviderValue": "Provedor: {0}",
+    "PluginUpdatedWithName": "{0} foi actualizado",
+    "PluginUninstalledWithName": "{0} foi desinstalado",
+    "PluginInstalledWithName": "{0} foi instalado",
+    "Playlists": "Listas de reproducción",
+    "Photos": "Fotos"
 }
 }

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

@@ -49,7 +49,7 @@
     "NotificationOptionAudioPlayback": "Audió lejátszás elkezdve",
     "NotificationOptionAudioPlayback": "Audió lejátszás elkezdve",
     "NotificationOptionAudioPlaybackStopped": "Audió lejátszás leállítva",
     "NotificationOptionAudioPlaybackStopped": "Audió lejátszás leállítva",
     "NotificationOptionCameraImageUploaded": "Kamera kép feltöltve",
     "NotificationOptionCameraImageUploaded": "Kamera kép feltöltve",
-    "NotificationOptionInstallationFailed": "Telepítési hiba",
+    "NotificationOptionInstallationFailed": "Telepítés sikertelen",
     "NotificationOptionNewLibraryContent": "Új tartalom hozzáadva",
     "NotificationOptionNewLibraryContent": "Új tartalom hozzáadva",
     "NotificationOptionPluginError": "Bővítmény hiba",
     "NotificationOptionPluginError": "Bővítmény hiba",
     "NotificationOptionPluginInstalled": "Bővítmény telepítve",
     "NotificationOptionPluginInstalled": "Bővítmény telepítve",

+ 2 - 2
Emby.Server.Implementations/Localization/Core/kk.json

@@ -109,14 +109,14 @@
     "TasksMaintenanceCategory": "Qyzmet körsetu",
     "TasksMaintenanceCategory": "Qyzmet körsetu",
     "Undefined": "Anyqtalmağan",
     "Undefined": "Anyqtalmağan",
     "Forced": "Mäjbürlı",
     "Forced": "Mäjbürlı",
-    "TaskDownloadMissingSubtitlesDescription": "Metaderekter teŋşelımderı negızınde joq subtitrlerdı Internetten ızdeidı.",
+    "TaskDownloadMissingSubtitlesDescription": "Metaderekter teŋşelımderı negızınde joq subtitrlerdı İnternetten ızdeidı.",
     "TaskRefreshChannelsDescription": "Internet-arnalar mälımetterın jaŋğyrtady.",
     "TaskRefreshChannelsDescription": "Internet-arnalar mälımetterın jaŋğyrtady.",
     "TaskCleanTranscodeDescription": "Bіr künnen asqan qaita kodtau faildaryn joiady.",
     "TaskCleanTranscodeDescription": "Bіr künnen asqan qaita kodtau faildaryn joiady.",
     "TaskUpdatePluginsDescription": "Avtomatty türde jaŋartuğa teŋşelgen plaginder üşın jaŋartulardy jüktep alady jäne ornatady.",
     "TaskUpdatePluginsDescription": "Avtomatty türde jaŋartuğa teŋşelgen plaginder üşın jaŋartulardy jüktep alady jäne ornatady.",
     "TaskRefreshPeopleDescription": "Tasyğyşhanadağy aktörler men rejisörler metaderekterın jaŋartady.",
     "TaskRefreshPeopleDescription": "Tasyğyşhanadağy aktörler men rejisörler metaderekterın jaŋartady.",
     "TaskCleanLogsDescription": "{0} künnen asqan jūrnal faildaryn joiady.",
     "TaskCleanLogsDescription": "{0} künnen asqan jūrnal faildaryn joiady.",
     "TaskRefreshLibraryDescription": "Tasyğyşhanadağy jaŋa faildardy skanerleidі jäne metaderekterdı jaŋğyrtady.",
     "TaskRefreshLibraryDescription": "Tasyğyşhanadağy jaŋa faildardy skanerleidі jäne metaderekterdı jaŋğyrtady.",
-    "TaskRefreshChapterImagesDescription": "Sahnalary bar beineler üşіn nobailar jasaidy.",
+    "TaskRefreshChapterImagesDescription": "Sahnalary bar beineler üşın nobailar jasaidy.",
     "TaskCleanCacheDescription": "Jüiede qajet emes keştelgen faildardy joiady.",
     "TaskCleanCacheDescription": "Jüiede qajet emes keştelgen faildardy joiady.",
     "TaskCleanActivityLogDescription": "Äreket jūrnalyndağy teŋşelgen jasynan asqan jazbalary joiady."
     "TaskCleanActivityLogDescription": "Äreket jūrnalyndağy teŋşelgen jasynan asqan jazbalary joiady."
 }
 }

+ 3 - 2
Emby.Server.Implementations/Localization/Core/th.json

@@ -50,7 +50,7 @@
     "HeaderFavoriteArtists": "ศิลปินที่ชื่นชอบ",
     "HeaderFavoriteArtists": "ศิลปินที่ชื่นชอบ",
     "HeaderFavoriteAlbums": "อัมบั้มที่ชื่นชอบ",
     "HeaderFavoriteAlbums": "อัมบั้มที่ชื่นชอบ",
     "HeaderContinueWatching": "ดูต่อ",
     "HeaderContinueWatching": "ดูต่อ",
-    "HeaderAlbumArtists": "อัลบั้มศิลปิน",
+    "HeaderAlbumArtists": "ศิลปินอัลบั้ม",
     "Genres": "ประเภท",
     "Genres": "ประเภท",
     "Folders": "โฟลเดอร์",
     "Folders": "โฟลเดอร์",
     "Favorites": "รายการโปรด",
     "Favorites": "รายการโปรด",
@@ -112,5 +112,6 @@
     "System": "ระบบ",
     "System": "ระบบ",
     "Sync": "ซิงค์",
     "Sync": "ซิงค์",
     "SubtitleDownloadFailureFromForItem": "ไม่สามารถดาวน์โหลดคำบรรยายจาก {0} สำหรับ {1} ได้",
     "SubtitleDownloadFailureFromForItem": "ไม่สามารถดาวน์โหลดคำบรรยายจาก {0} สำหรับ {1} ได้",
-    "StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่"
+    "StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่",
+    "Default": "ค่าเริ่มต้น"
 }
 }

+ 1 - 1
Emby.Server.Implementations/Localization/LocalizationManager.cs

@@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Localization
 
 
         private List<CultureDto> _cultures;
         private List<CultureDto> _cultures;
 
 
-        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="LocalizationManager" /> class.
         /// Initializes a new instance of the <see cref="LocalizationManager" /> class.

+ 4 - 4
Emby.Server.Implementations/Plugins/PluginManager.cs

@@ -34,7 +34,7 @@ namespace Emby.Server.Implementations.Plugins
         private readonly ILogger<PluginManager> _logger;
         private readonly ILogger<PluginManager> _logger;
         private readonly IApplicationHost _appHost;
         private readonly IApplicationHost _appHost;
         private readonly ServerConfiguration _config;
         private readonly ServerConfiguration _config;
-        private readonly IList<LocalPlugin> _plugins;
+        private readonly List<LocalPlugin> _plugins;
         private readonly Version _minimumVersion;
         private readonly Version _minimumVersion;
 
 
         private IHttpClientFactory? _httpClientFactory;
         private IHttpClientFactory? _httpClientFactory;
@@ -70,7 +70,7 @@ namespace Emby.Server.Implementations.Plugins
             _logger = logger ?? throw new ArgumentNullException(nameof(logger));
             _logger = logger ?? throw new ArgumentNullException(nameof(logger));
             _pluginsPath = pluginsPath;
             _pluginsPath = pluginsPath;
             _appVersion = appVersion ?? throw new ArgumentNullException(nameof(appVersion));
             _appVersion = appVersion ?? throw new ArgumentNullException(nameof(appVersion));
-            _jsonOptions = new JsonSerializerOptions(JsonDefaults.GetOptions())
+            _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options)
             {
             {
                 WriteIndented = true
                 WriteIndented = true
             };
             };
@@ -94,7 +94,7 @@ namespace Emby.Server.Implementations.Plugins
         /// <summary>
         /// <summary>
         /// Gets the Plugins.
         /// Gets the Plugins.
         /// </summary>
         /// </summary>
-        public IList<LocalPlugin> Plugins => _plugins;
+        public IReadOnlyList<LocalPlugin> Plugins => _plugins;
 
 
         /// <summary>
         /// <summary>
         /// Returns all the assemblies.
         /// Returns all the assemblies.
@@ -678,7 +678,7 @@ namespace Emby.Server.Implementations.Plugins
                 var entry = versions[x];
                 var entry = versions[x];
                 if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase))
                 if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase))
                 {
                 {
-                    entry.DllFiles.AddRange(Directory.EnumerateFiles(entry.Path, "*.dll", SearchOption.AllDirectories));
+                    entry.DllFiles = Directory.GetFiles(entry.Path, "*.dll", SearchOption.AllDirectories);
                     if (entry.IsEnabledAndSupported)
                     if (entry.IsEnabledAndSupported)
                     {
                     {
                         lastName = entry.Name;
                         lastName = entry.Name;

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

@@ -69,7 +69,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// <summary>
         /// <summary>
         /// The options for the json Serializer.
         /// The options for the json Serializer.
         /// </summary>
         /// </summary>
-        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class.
         /// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class.

+ 1 - 1
Emby.Server.Implementations/Udp/UdpServer.cs

@@ -79,7 +79,7 @@ namespace Emby.Server.Implementations.Udp
         /// Starts the specified port.
         /// Starts the specified port.
         /// </summary>
         /// </summary>
         /// <param name="port">The port.</param>
         /// <param name="port">The port.</param>
-        /// <param name="cancellationToken"></param>
+        /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
         public void Start(int port, CancellationToken cancellationToken)
         public void Start(int port, CancellationToken cancellationToken)
         {
         {
             _endpoint = new IPEndPoint(IPAddress.Any, port);
             _endpoint = new IPEndPoint(IPAddress.Any, port);

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

@@ -92,7 +92,7 @@ namespace Emby.Server.Implementations.Updates
             _httpClientFactory = httpClientFactory;
             _httpClientFactory = httpClientFactory;
             _config = config;
             _config = config;
             _zipClient = zipClient;
             _zipClient = zipClient;
-            _jsonSerializerOptions = JsonDefaults.GetOptions();
+            _jsonSerializerOptions = JsonDefaults.Options;
             _pluginManager = pluginManager;
             _pluginManager = pluginManager;
         }
         }
 
 

+ 4 - 4
Jellyfin.Api/Controllers/AudioController.cs

@@ -122,7 +122,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? height,
             [FromQuery] int? height,
             [FromQuery] int? videoBitRate,
             [FromQuery] int? videoBitRate,
             [FromQuery] int? subtitleStreamIndex,
             [FromQuery] int? subtitleStreamIndex,
-            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
             [FromQuery] int? maxRefFrames,
             [FromQuery] int? maxRefFrames,
             [FromQuery] int? maxVideoBitDepth,
             [FromQuery] int? maxVideoBitDepth,
             [FromQuery] bool? requireAvc,
             [FromQuery] bool? requireAvc,
@@ -174,7 +174,7 @@ namespace Jellyfin.Api.Controllers
                 Height = height,
                 Height = height,
                 VideoBitRate = videoBitRate,
                 VideoBitRate = videoBitRate,
                 SubtitleStreamIndex = subtitleStreamIndex,
                 SubtitleStreamIndex = subtitleStreamIndex,
-                SubtitleMethod = subtitleMethod,
+                SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
                 MaxRefFrames = maxRefFrames,
                 MaxRefFrames = maxRefFrames,
                 MaxVideoBitDepth = maxVideoBitDepth,
                 MaxVideoBitDepth = maxVideoBitDepth,
                 RequireAvc = requireAvc ?? true,
                 RequireAvc = requireAvc ?? true,
@@ -287,7 +287,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? height,
             [FromQuery] int? height,
             [FromQuery] int? videoBitRate,
             [FromQuery] int? videoBitRate,
             [FromQuery] int? subtitleStreamIndex,
             [FromQuery] int? subtitleStreamIndex,
-            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
             [FromQuery] int? maxRefFrames,
             [FromQuery] int? maxRefFrames,
             [FromQuery] int? maxVideoBitDepth,
             [FromQuery] int? maxVideoBitDepth,
             [FromQuery] bool? requireAvc,
             [FromQuery] bool? requireAvc,
@@ -339,7 +339,7 @@ namespace Jellyfin.Api.Controllers
                 Height = height,
                 Height = height,
                 VideoBitRate = videoBitRate,
                 VideoBitRate = videoBitRate,
                 SubtitleStreamIndex = subtitleStreamIndex,
                 SubtitleStreamIndex = subtitleStreamIndex,
-                SubtitleMethod = subtitleMethod,
+                SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
                 MaxRefFrames = maxRefFrames,
                 MaxRefFrames = maxRefFrames,
                 MaxVideoBitDepth = maxVideoBitDepth,
                 MaxVideoBitDepth = maxVideoBitDepth,
                 RequireAvc = requireAvc ?? true,
                 RequireAvc = requireAvc ?? true,

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

@@ -25,7 +25,7 @@ namespace Jellyfin.Api.Controllers
         private readonly IServerConfigurationManager _configurationManager;
         private readonly IServerConfigurationManager _configurationManager;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IMediaEncoder _mediaEncoder;
 
 
-        private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions();
+        private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.Options;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="ConfigurationController"/> class.
         /// Initializes a new instance of the <see cref="ConfigurationController"/> class.

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

@@ -95,9 +95,9 @@ namespace Jellyfin.Api.Controllers
             return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1));
             return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1));
         }
         }
 
 
-        private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin? plugin)
+        private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin plugin)
         {
         {
-            if (plugin?.Instance is not IHasWebPages hasWebPages)
+            if (plugin.Instance is not IHasWebPages hasWebPages)
             {
             {
                 return Enumerable.Empty<Tuple<PluginPageInfo, IPlugin>>();
                 return Enumerable.Empty<Tuple<PluginPageInfo, IPlugin>>();
             }
             }

+ 24 - 24
Jellyfin.Api/Controllers/DynamicHlsController.cs

@@ -203,7 +203,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? height,
             [FromQuery] int? height,
             [FromQuery] int? videoBitRate,
             [FromQuery] int? videoBitRate,
             [FromQuery] int? subtitleStreamIndex,
             [FromQuery] int? subtitleStreamIndex,
-            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
             [FromQuery] int? maxRefFrames,
             [FromQuery] int? maxRefFrames,
             [FromQuery] int? maxVideoBitDepth,
             [FromQuery] int? maxVideoBitDepth,
             [FromQuery] bool? requireAvc,
             [FromQuery] bool? requireAvc,
@@ -218,7 +218,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? transcodeReasons,
             [FromQuery] string? transcodeReasons,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? videoStreamIndex,
             [FromQuery] int? videoStreamIndex,
-            [FromQuery] EncodingContext context,
+            [FromQuery] EncodingContext? context,
             [FromQuery] Dictionary<string, string> streamOptions,
             [FromQuery] Dictionary<string, string> streamOptions,
             [FromQuery] bool enableAdaptiveBitrateStreaming = true)
             [FromQuery] bool enableAdaptiveBitrateStreaming = true)
         {
         {
@@ -255,7 +255,7 @@ namespace Jellyfin.Api.Controllers
                 Height = height,
                 Height = height,
                 VideoBitRate = videoBitRate,
                 VideoBitRate = videoBitRate,
                 SubtitleStreamIndex = subtitleStreamIndex,
                 SubtitleStreamIndex = subtitleStreamIndex,
-                SubtitleMethod = subtitleMethod,
+                SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
                 MaxRefFrames = maxRefFrames,
                 MaxRefFrames = maxRefFrames,
                 MaxVideoBitDepth = maxVideoBitDepth,
                 MaxVideoBitDepth = maxVideoBitDepth,
                 RequireAvc = requireAvc ?? true,
                 RequireAvc = requireAvc ?? true,
@@ -270,7 +270,7 @@ namespace Jellyfin.Api.Controllers
                 TranscodeReasons = transcodeReasons,
                 TranscodeReasons = transcodeReasons,
                 AudioStreamIndex = audioStreamIndex,
                 AudioStreamIndex = audioStreamIndex,
                 VideoStreamIndex = videoStreamIndex,
                 VideoStreamIndex = videoStreamIndex,
-                Context = context,
+                Context = context ?? EncodingContext.Streaming,
                 StreamOptions = streamOptions,
                 StreamOptions = streamOptions,
                 EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
                 EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
             };
             };
@@ -370,7 +370,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? height,
             [FromQuery] int? height,
             [FromQuery] int? videoBitRate,
             [FromQuery] int? videoBitRate,
             [FromQuery] int? subtitleStreamIndex,
             [FromQuery] int? subtitleStreamIndex,
-            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
             [FromQuery] int? maxRefFrames,
             [FromQuery] int? maxRefFrames,
             [FromQuery] int? maxVideoBitDepth,
             [FromQuery] int? maxVideoBitDepth,
             [FromQuery] bool? requireAvc,
             [FromQuery] bool? requireAvc,
@@ -385,7 +385,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? transcodeReasons,
             [FromQuery] string? transcodeReasons,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? videoStreamIndex,
             [FromQuery] int? videoStreamIndex,
-            [FromQuery] EncodingContext context,
+            [FromQuery] EncodingContext? context,
             [FromQuery] Dictionary<string, string> streamOptions,
             [FromQuery] Dictionary<string, string> streamOptions,
             [FromQuery] bool enableAdaptiveBitrateStreaming = true)
             [FromQuery] bool enableAdaptiveBitrateStreaming = true)
         {
         {
@@ -422,7 +422,7 @@ namespace Jellyfin.Api.Controllers
                 Height = height,
                 Height = height,
                 VideoBitRate = videoBitRate,
                 VideoBitRate = videoBitRate,
                 SubtitleStreamIndex = subtitleStreamIndex,
                 SubtitleStreamIndex = subtitleStreamIndex,
-                SubtitleMethod = subtitleMethod,
+                SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
                 MaxRefFrames = maxRefFrames,
                 MaxRefFrames = maxRefFrames,
                 MaxVideoBitDepth = maxVideoBitDepth,
                 MaxVideoBitDepth = maxVideoBitDepth,
                 RequireAvc = requireAvc ?? true,
                 RequireAvc = requireAvc ?? true,
@@ -437,7 +437,7 @@ namespace Jellyfin.Api.Controllers
                 TranscodeReasons = transcodeReasons,
                 TranscodeReasons = transcodeReasons,
                 AudioStreamIndex = audioStreamIndex,
                 AudioStreamIndex = audioStreamIndex,
                 VideoStreamIndex = videoStreamIndex,
                 VideoStreamIndex = videoStreamIndex,
-                Context = context,
+                Context = context ?? EncodingContext.Streaming,
                 StreamOptions = streamOptions,
                 StreamOptions = streamOptions,
                 EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
                 EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
             };
             };
@@ -533,7 +533,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? height,
             [FromQuery] int? height,
             [FromQuery] int? videoBitRate,
             [FromQuery] int? videoBitRate,
             [FromQuery] int? subtitleStreamIndex,
             [FromQuery] int? subtitleStreamIndex,
-            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
             [FromQuery] int? maxRefFrames,
             [FromQuery] int? maxRefFrames,
             [FromQuery] int? maxVideoBitDepth,
             [FromQuery] int? maxVideoBitDepth,
             [FromQuery] bool? requireAvc,
             [FromQuery] bool? requireAvc,
@@ -548,7 +548,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? transcodeReasons,
             [FromQuery] string? transcodeReasons,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? videoStreamIndex,
             [FromQuery] int? videoStreamIndex,
-            [FromQuery] EncodingContext context,
+            [FromQuery] EncodingContext? context,
             [FromQuery] Dictionary<string, string> streamOptions)
             [FromQuery] Dictionary<string, string> streamOptions)
         {
         {
             var cancellationTokenSource = new CancellationTokenSource();
             var cancellationTokenSource = new CancellationTokenSource();
@@ -585,7 +585,7 @@ namespace Jellyfin.Api.Controllers
                 Height = height,
                 Height = height,
                 VideoBitRate = videoBitRate,
                 VideoBitRate = videoBitRate,
                 SubtitleStreamIndex = subtitleStreamIndex,
                 SubtitleStreamIndex = subtitleStreamIndex,
-                SubtitleMethod = subtitleMethod,
+                SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
                 MaxRefFrames = maxRefFrames,
                 MaxRefFrames = maxRefFrames,
                 MaxVideoBitDepth = maxVideoBitDepth,
                 MaxVideoBitDepth = maxVideoBitDepth,
                 RequireAvc = requireAvc ?? true,
                 RequireAvc = requireAvc ?? true,
@@ -600,7 +600,7 @@ namespace Jellyfin.Api.Controllers
                 TranscodeReasons = transcodeReasons,
                 TranscodeReasons = transcodeReasons,
                 AudioStreamIndex = audioStreamIndex,
                 AudioStreamIndex = audioStreamIndex,
                 VideoStreamIndex = videoStreamIndex,
                 VideoStreamIndex = videoStreamIndex,
-                Context = context,
+                Context = context ?? EncodingContext.Streaming,
                 StreamOptions = streamOptions
                 StreamOptions = streamOptions
             };
             };
 
 
@@ -698,7 +698,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? height,
             [FromQuery] int? height,
             [FromQuery] int? videoBitRate,
             [FromQuery] int? videoBitRate,
             [FromQuery] int? subtitleStreamIndex,
             [FromQuery] int? subtitleStreamIndex,
-            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
             [FromQuery] int? maxRefFrames,
             [FromQuery] int? maxRefFrames,
             [FromQuery] int? maxVideoBitDepth,
             [FromQuery] int? maxVideoBitDepth,
             [FromQuery] bool? requireAvc,
             [FromQuery] bool? requireAvc,
@@ -713,7 +713,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? transcodeReasons,
             [FromQuery] string? transcodeReasons,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? videoStreamIndex,
             [FromQuery] int? videoStreamIndex,
-            [FromQuery] EncodingContext context,
+            [FromQuery] EncodingContext? context,
             [FromQuery] Dictionary<string, string> streamOptions)
             [FromQuery] Dictionary<string, string> streamOptions)
         {
         {
             var cancellationTokenSource = new CancellationTokenSource();
             var cancellationTokenSource = new CancellationTokenSource();
@@ -750,7 +750,7 @@ namespace Jellyfin.Api.Controllers
                 Height = height,
                 Height = height,
                 VideoBitRate = videoBitRate,
                 VideoBitRate = videoBitRate,
                 SubtitleStreamIndex = subtitleStreamIndex,
                 SubtitleStreamIndex = subtitleStreamIndex,
-                SubtitleMethod = subtitleMethod,
+                SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
                 MaxRefFrames = maxRefFrames,
                 MaxRefFrames = maxRefFrames,
                 MaxVideoBitDepth = maxVideoBitDepth,
                 MaxVideoBitDepth = maxVideoBitDepth,
                 RequireAvc = requireAvc ?? true,
                 RequireAvc = requireAvc ?? true,
@@ -765,7 +765,7 @@ namespace Jellyfin.Api.Controllers
                 TranscodeReasons = transcodeReasons,
                 TranscodeReasons = transcodeReasons,
                 AudioStreamIndex = audioStreamIndex,
                 AudioStreamIndex = audioStreamIndex,
                 VideoStreamIndex = videoStreamIndex,
                 VideoStreamIndex = videoStreamIndex,
-                Context = context,
+                Context = context ?? EncodingContext.Streaming,
                 StreamOptions = streamOptions
                 StreamOptions = streamOptions
             };
             };
 
 
@@ -868,7 +868,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? height,
             [FromQuery] int? height,
             [FromQuery] int? videoBitRate,
             [FromQuery] int? videoBitRate,
             [FromQuery] int? subtitleStreamIndex,
             [FromQuery] int? subtitleStreamIndex,
-            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
             [FromQuery] int? maxRefFrames,
             [FromQuery] int? maxRefFrames,
             [FromQuery] int? maxVideoBitDepth,
             [FromQuery] int? maxVideoBitDepth,
             [FromQuery] bool? requireAvc,
             [FromQuery] bool? requireAvc,
@@ -883,7 +883,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? transcodeReasons,
             [FromQuery] string? transcodeReasons,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? videoStreamIndex,
             [FromQuery] int? videoStreamIndex,
-            [FromQuery] EncodingContext context,
+            [FromQuery] EncodingContext? context,
             [FromQuery] Dictionary<string, string> streamOptions)
             [FromQuery] Dictionary<string, string> streamOptions)
         {
         {
             var streamingRequest = new VideoRequestDto
             var streamingRequest = new VideoRequestDto
@@ -920,7 +920,7 @@ namespace Jellyfin.Api.Controllers
                 Height = height,
                 Height = height,
                 VideoBitRate = videoBitRate,
                 VideoBitRate = videoBitRate,
                 SubtitleStreamIndex = subtitleStreamIndex,
                 SubtitleStreamIndex = subtitleStreamIndex,
-                SubtitleMethod = subtitleMethod,
+                SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
                 MaxRefFrames = maxRefFrames,
                 MaxRefFrames = maxRefFrames,
                 MaxVideoBitDepth = maxVideoBitDepth,
                 MaxVideoBitDepth = maxVideoBitDepth,
                 RequireAvc = requireAvc ?? true,
                 RequireAvc = requireAvc ?? true,
@@ -935,7 +935,7 @@ namespace Jellyfin.Api.Controllers
                 TranscodeReasons = transcodeReasons,
                 TranscodeReasons = transcodeReasons,
                 AudioStreamIndex = audioStreamIndex,
                 AudioStreamIndex = audioStreamIndex,
                 VideoStreamIndex = videoStreamIndex,
                 VideoStreamIndex = videoStreamIndex,
-                Context = context,
+                Context = context ?? EncodingContext.Streaming,
                 StreamOptions = streamOptions
                 StreamOptions = streamOptions
             };
             };
 
 
@@ -1040,7 +1040,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? height,
             [FromQuery] int? height,
             [FromQuery] int? videoBitRate,
             [FromQuery] int? videoBitRate,
             [FromQuery] int? subtitleStreamIndex,
             [FromQuery] int? subtitleStreamIndex,
-            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
             [FromQuery] int? maxRefFrames,
             [FromQuery] int? maxRefFrames,
             [FromQuery] int? maxVideoBitDepth,
             [FromQuery] int? maxVideoBitDepth,
             [FromQuery] bool? requireAvc,
             [FromQuery] bool? requireAvc,
@@ -1055,7 +1055,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? transcodeReasons,
             [FromQuery] string? transcodeReasons,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? videoStreamIndex,
             [FromQuery] int? videoStreamIndex,
-            [FromQuery] EncodingContext context,
+            [FromQuery] EncodingContext? context,
             [FromQuery] Dictionary<string, string> streamOptions)
             [FromQuery] Dictionary<string, string> streamOptions)
         {
         {
             var streamingRequest = new StreamingRequestDto
             var streamingRequest = new StreamingRequestDto
@@ -1092,7 +1092,7 @@ namespace Jellyfin.Api.Controllers
                 Height = height,
                 Height = height,
                 VideoBitRate = videoBitRate,
                 VideoBitRate = videoBitRate,
                 SubtitleStreamIndex = subtitleStreamIndex,
                 SubtitleStreamIndex = subtitleStreamIndex,
-                SubtitleMethod = subtitleMethod,
+                SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
                 MaxRefFrames = maxRefFrames,
                 MaxRefFrames = maxRefFrames,
                 MaxVideoBitDepth = maxVideoBitDepth,
                 MaxVideoBitDepth = maxVideoBitDepth,
                 RequireAvc = requireAvc ?? true,
                 RequireAvc = requireAvc ?? true,
@@ -1107,7 +1107,7 @@ namespace Jellyfin.Api.Controllers
                 TranscodeReasons = transcodeReasons,
                 TranscodeReasons = transcodeReasons,
                 AudioStreamIndex = audioStreamIndex,
                 AudioStreamIndex = audioStreamIndex,
                 VideoStreamIndex = videoStreamIndex,
                 VideoStreamIndex = videoStreamIndex,
-                Context = context,
+                Context = context ?? EncodingContext.Streaming,
                 StreamOptions = streamOptions
                 StreamOptions = streamOptions
             };
             };
 
 

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

@@ -96,7 +96,9 @@ namespace Jellyfin.Api.Controllers
         [HttpDelete("Videos/ActiveEncodings")]
         [HttpDelete("Videos/ActiveEncodings")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult StopEncodingProcess([FromQuery] string deviceId, [FromQuery] string playSessionId)
+        public ActionResult StopEncodingProcess(
+            [FromQuery, Required] string deviceId,
+            [FromQuery, Required] string playSessionId)
         {
         {
             _transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true);
             _transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true);
             return NoContent();
             return NoContent();

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

@@ -402,7 +402,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] Guid itemId,
             [FromRoute, Required] Guid itemId,
             [FromRoute, Required] ImageType imageType,
             [FromRoute, Required] ImageType imageType,
             [FromRoute, Required] int imageIndex,
             [FromRoute, Required] int imageIndex,
-            [FromQuery] int newIndex)
+            [FromQuery, Required] int newIndex)
         {
         {
             var item = _libraryManager.GetItemById(itemId);
             var item = _libraryManager.GetItemById(itemId);
             if (item == null)
             if (item == null)
@@ -751,7 +751,7 @@ namespace Jellyfin.Api.Controllers
         public async Task<ActionResult> GetArtistImage(
         public async Task<ActionResult> GetArtistImage(
             [FromRoute, Required] string name,
             [FromRoute, Required] string name,
             [FromRoute, Required] ImageType imageType,
             [FromRoute, Required] ImageType imageType,
-            [FromQuery] string tag,
+            [FromQuery] string? tag,
             [FromQuery] ImageFormat? format,
             [FromQuery] ImageFormat? format,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxHeight,
             [FromQuery] int? maxHeight,
@@ -830,7 +830,7 @@ namespace Jellyfin.Api.Controllers
         public async Task<ActionResult> GetGenreImage(
         public async Task<ActionResult> GetGenreImage(
             [FromRoute, Required] string name,
             [FromRoute, Required] string name,
             [FromRoute, Required] ImageType imageType,
             [FromRoute, Required] ImageType imageType,
-            [FromQuery] string tag,
+            [FromQuery] string? tag,
             [FromQuery] ImageFormat? format,
             [FromQuery] ImageFormat? format,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxHeight,
             [FromQuery] int? maxHeight,
@@ -910,7 +910,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] string name,
             [FromRoute, Required] string name,
             [FromRoute, Required] ImageType imageType,
             [FromRoute, Required] ImageType imageType,
             [FromRoute, Required] int imageIndex,
             [FromRoute, Required] int imageIndex,
-            [FromQuery] string tag,
+            [FromQuery] string? tag,
             [FromQuery] ImageFormat? format,
             [FromQuery] ImageFormat? format,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxHeight,
             [FromQuery] int? maxHeight,
@@ -988,7 +988,7 @@ namespace Jellyfin.Api.Controllers
         public async Task<ActionResult> GetMusicGenreImage(
         public async Task<ActionResult> GetMusicGenreImage(
             [FromRoute, Required] string name,
             [FromRoute, Required] string name,
             [FromRoute, Required] ImageType imageType,
             [FromRoute, Required] ImageType imageType,
-            [FromQuery] string tag,
+            [FromQuery] string? tag,
             [FromQuery] ImageFormat? format,
             [FromQuery] ImageFormat? format,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxHeight,
             [FromQuery] int? maxHeight,
@@ -1068,7 +1068,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] string name,
             [FromRoute, Required] string name,
             [FromRoute, Required] ImageType imageType,
             [FromRoute, Required] ImageType imageType,
             [FromRoute, Required] int imageIndex,
             [FromRoute, Required] int imageIndex,
-            [FromQuery] string tag,
+            [FromQuery] string? tag,
             [FromQuery] ImageFormat? format,
             [FromQuery] ImageFormat? format,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxHeight,
             [FromQuery] int? maxHeight,
@@ -1146,7 +1146,7 @@ namespace Jellyfin.Api.Controllers
         public async Task<ActionResult> GetPersonImage(
         public async Task<ActionResult> GetPersonImage(
             [FromRoute, Required] string name,
             [FromRoute, Required] string name,
             [FromRoute, Required] ImageType imageType,
             [FromRoute, Required] ImageType imageType,
-            [FromQuery] string tag,
+            [FromQuery] string? tag,
             [FromQuery] ImageFormat? format,
             [FromQuery] ImageFormat? format,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxHeight,
             [FromQuery] int? maxHeight,
@@ -1226,7 +1226,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] string name,
             [FromRoute, Required] string name,
             [FromRoute, Required] ImageType imageType,
             [FromRoute, Required] ImageType imageType,
             [FromRoute, Required] int imageIndex,
             [FromRoute, Required] int imageIndex,
-            [FromQuery] string tag,
+            [FromQuery] string? tag,
             [FromQuery] ImageFormat? format,
             [FromQuery] ImageFormat? format,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxHeight,
             [FromQuery] int? maxHeight,

+ 82 - 8
Jellyfin.Api/Controllers/InstantMixController.cs

@@ -86,7 +86,7 @@ namespace Jellyfin.Api.Controllers
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Creates an instant playlist based on a given song.
+        /// Creates an instant playlist based on a given album.
         /// </summary>
         /// </summary>
         /// <param name="id">The item id.</param>
         /// <param name="id">The item id.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -122,7 +122,7 @@ namespace Jellyfin.Api.Controllers
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Creates an instant playlist based on a given song.
+        /// Creates an instant playlist based on a given playlist.
         /// </summary>
         /// </summary>
         /// <param name="id">The item id.</param>
         /// <param name="id">The item id.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -158,7 +158,7 @@ namespace Jellyfin.Api.Controllers
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Creates an instant playlist based on a given song.
+        /// Creates an instant playlist based on a given genre.
         /// </summary>
         /// </summary>
         /// <param name="name">The genre name.</param>
         /// <param name="name">The genre name.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -172,7 +172,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
         [HttpGet("MusicGenres/{name}/InstantMix")]
         [HttpGet("MusicGenres/{name}/InstantMix")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenre(
+        public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreByName(
             [FromRoute, Required] string name,
             [FromRoute, Required] string name,
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
             [FromQuery] int? limit,
             [FromQuery] int? limit,
@@ -193,7 +193,7 @@ namespace Jellyfin.Api.Controllers
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Creates an instant playlist based on a given song.
+        /// Creates an instant playlist based on a given artist.
         /// </summary>
         /// </summary>
         /// <param name="id">The item id.</param>
         /// <param name="id">The item id.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -229,7 +229,7 @@ namespace Jellyfin.Api.Controllers
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Creates an instant playlist based on a given song.
+        /// Creates an instant playlist based on a given genre.
         /// </summary>
         /// </summary>
         /// <param name="id">The item id.</param>
         /// <param name="id">The item id.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -243,7 +243,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
         [HttpGet("MusicGenres/{id}/InstantMix")]
         [HttpGet("MusicGenres/{id}/InstantMix")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres(
+        public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById(
             [FromRoute, Required] Guid id,
             [FromRoute, Required] Guid id,
             [FromQuery] Guid? userId,
             [FromQuery] Guid? userId,
             [FromQuery] int? limit,
             [FromQuery] int? limit,
@@ -265,7 +265,7 @@ namespace Jellyfin.Api.Controllers
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Creates an instant playlist based on a given song.
+        /// Creates an instant playlist based on a given item.
         /// </summary>
         /// </summary>
         /// <param name="id">The item id.</param>
         /// <param name="id">The item id.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -300,6 +300,80 @@ namespace Jellyfin.Api.Controllers
             return GetResult(items, user, limit, dtoOptions);
             return GetResult(items, user, limit, dtoOptions);
         }
         }
 
 
+        /// <summary>
+        /// Creates an instant playlist based on a given artist.
+        /// </summary>
+        /// <param name="id">The item id.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+        /// <param name="enableImages">Optional. Include image information in output.</param>
+        /// <param name="enableUserData">Optional. Include user data.</param>
+        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <response code="200">Instant playlist returned.</response>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
+        [HttpGet("Artists/InstantMix")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [Obsolete("Use GetInstantMixFromArtists")]
+        public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists2(
+            [FromQuery, Required] Guid id,
+            [FromQuery] Guid? userId,
+            [FromQuery] int? limit,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+            [FromQuery] bool? enableImages,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
+        {
+            return GetInstantMixFromArtists(
+                id,
+                userId,
+                limit,
+                fields,
+                enableImages,
+                enableUserData,
+                imageTypeLimit,
+                enableImageTypes);
+        }
+
+        /// <summary>
+        /// Creates an instant playlist based on a given genre.
+        /// </summary>
+        /// <param name="id">The item id.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+        /// <param name="enableImages">Optional. Include image information in output.</param>
+        /// <param name="enableUserData">Optional. Include user data.</param>
+        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <response code="200">Instant playlist returned.</response>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
+        [HttpGet("MusicGenres/InstantMix")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [Obsolete("Use GetInstantMixFromMusicGenres instead")]
+        public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById2(
+            [FromQuery, Required] Guid id,
+            [FromQuery] Guid? userId,
+            [FromQuery] int? limit,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+            [FromQuery] bool? enableImages,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
+        {
+            return GetInstantMixFromMusicGenreById(
+                id,
+                userId,
+                limit,
+                fields,
+                enableImages,
+                enableUserData,
+                imageTypeLimit,
+                enableImageTypes);
+        }
+
         private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
         private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
         {
         {
             var list = items;
             var list = items;

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

@@ -344,11 +344,12 @@ namespace Jellyfin.Api.Controllers
             Directory.CreateDirectory(directory);
             Directory.CreateDirectory(directory);
             using (var stream = result.Content)
             using (var stream = result.Content)
             {
             {
+                // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
                 await using var fileStream = new FileStream(
                 await using var fileStream = new FileStream(
                     fullCachePath,
                     fullCachePath,
                     FileMode.Create,
                     FileMode.Create,
                     FileAccess.Write,
                     FileAccess.Write,
-                    FileShare.Read,
+                    FileShare.None,
                     IODefaults.FileStreamBufferSize,
                     IODefaults.FileStreamBufferSize,
                     true);
                     true);
 
 

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

@@ -195,7 +195,7 @@ namespace Jellyfin.Api.Controllers
         [HttpPost("Items/{itemId}/ContentType")]
         [HttpPost("Items/{itemId}/ContentType")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string contentType)
+        public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType)
         {
         {
             var item = _libraryManager.GetItemById(itemId);
             var item = _libraryManager.GetItemById(itemId);
             if (item == null)
             if (item == null)

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

@@ -303,7 +303,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// </summary>
         /// <response code="204">Library scan started.</response>
         /// <response code="204">Library scan started.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpGet("Library/Refresh")]
+        [HttpPost("Library/Refresh")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> RefreshLibrary()
         public async Task<ActionResult> RefreshLibrary()
@@ -590,15 +590,15 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// <summary>
         /// Reports that new movies have been added by an external source.
         /// Reports that new movies have been added by an external source.
         /// </summary>
         /// </summary>
-        /// <param name="updates">A list of updated media paths.</param>
+        /// <param name="dto">The update paths.</param>
         /// <response code="204">Report success.</response>
         /// <response code="204">Report success.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Library/Media/Updated")]
         [HttpPost("Library/Media/Updated")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto[] updates)
+        public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto dto)
         {
         {
-            foreach (var item in updates)
+            foreach (var item in dto.Updates)
             {
             {
                 _libraryMonitor.ReportFileSystemChanged(item.Path);
                 _libraryMonitor.ReportFileSystemChanged(item.Path);
             }
             }
@@ -777,7 +777,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo(
         public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo(
             [FromQuery] string? libraryContentType,
             [FromQuery] string? libraryContentType,
-            [FromQuery] bool isNewLibrary)
+            [FromQuery] bool isNewLibrary = false)
         {
         {
             var result = new LibraryOptionsResultDto();
             var result = new LibraryOptionsResultDto();
 
 

+ 5 - 8
Jellyfin.Api/Controllers/LibraryStructureController.cs

@@ -241,23 +241,20 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// <summary>
         /// Updates a media path.
         /// Updates a media path.
         /// </summary>
         /// </summary>
-        /// <param name="name">The name of the library.</param>
-        /// <param name="pathInfo">The path info.</param>
+        /// <param name="mediaPathRequestDto">The name of the library and path infos.</param>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         /// <response code="204">Media path updated.</response>
         /// <response code="204">Media path updated.</response>
         /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
         /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
         [HttpPost("Paths/Update")]
         [HttpPost("Paths/Update")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult UpdateMediaPath(
-            [FromQuery] string? name,
-            [FromBody] MediaPathInfo? pathInfo)
+        public ActionResult UpdateMediaPath([FromBody, Required] UpdateMediaPathRequestDto mediaPathRequestDto)
         {
         {
-            if (string.IsNullOrWhiteSpace(name))
+            if (string.IsNullOrWhiteSpace(mediaPathRequestDto.Name))
             {
             {
-                throw new ArgumentNullException(nameof(name));
+                throw new ArgumentNullException(nameof(mediaPathRequestDto), "Name must not be null or empty");
             }
             }
 
 
-            _libraryManager.UpdateMediaPath(name, pathInfo);
+            _libraryManager.UpdateMediaPath(mediaPathRequestDto.Name, mediaPathRequestDto.PathInfo);
             return NoContent();
             return NoContent();
         }
         }
 
 

+ 7 - 14
Jellyfin.Api/Controllers/NotificationsController.cs

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
@@ -86,26 +87,19 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// <summary>
         /// Sends a notification to all admins.
         /// Sends a notification to all admins.
         /// </summary>
         /// </summary>
-        /// <param name="url">The URL of the notification.</param>
-        /// <param name="level">The level of the notification.</param>
-        /// <param name="name">The name of the notification.</param>
-        /// <param name="description">The description of the notification.</param>
+        /// <param name="notificationDto">The notification request.</param>
         /// <response code="204">Notification sent.</response>
         /// <response code="204">Notification sent.</response>
         /// <returns>A <cref see="NoContentResult"/>.</returns>
         /// <returns>A <cref see="NoContentResult"/>.</returns>
         [HttpPost("Admin")]
         [HttpPost("Admin")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult CreateAdminNotification(
-            [FromQuery] string? url,
-            [FromQuery] NotificationLevel? level,
-            [FromQuery] string name = "",
-            [FromQuery] string description = "")
+        public ActionResult CreateAdminNotification([FromBody, Required] AdminNotificationDto notificationDto)
         {
         {
             var notification = new NotificationRequest
             var notification = new NotificationRequest
             {
             {
-                Name = name,
-                Description = description,
-                Url = url,
-                Level = level ?? NotificationLevel.Normal,
+                Name = notificationDto.Name,
+                Description = notificationDto.Description,
+                Url = notificationDto.Url,
+                Level = notificationDto.NotificationLevel ?? NotificationLevel.Normal,
                 UserIds = _userManager.Users
                 UserIds = _userManager.Users
                     .Where(user => user.HasPermission(PermissionKind.IsAdministrator))
                     .Where(user => user.HasPermission(PermissionKind.IsAdministrator))
                     .Select(user => user.Id)
                     .Select(user => user.Id)
@@ -114,7 +108,6 @@ namespace Jellyfin.Api.Controllers
             };
             };
 
 
             _notificationManager.SendNotification(notification, CancellationToken.None);
             _notificationManager.SendNotification(notification, CancellationToken.None);
-
             return NoContent();
             return NoContent();
         }
         }
 
 

+ 10 - 10
Jellyfin.Api/Controllers/PlaystateController.cs

@@ -152,7 +152,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Sessions/Playing/Ping")]
         [HttpPost("Sessions/Playing/Ping")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult PingPlaybackSession([FromQuery] string playSessionId)
+        public ActionResult PingPlaybackSession([FromQuery, Required] string playSessionId)
         {
         {
             _transcodingJobHelper.PingTranscodingJob(playSessionId, null);
             _transcodingJobHelper.PingTranscodingJob(playSessionId, null);
             return NoContent();
             return NoContent();
@@ -202,9 +202,9 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? mediaSourceId,
             [FromQuery] string? mediaSourceId,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? subtitleStreamIndex,
             [FromQuery] int? subtitleStreamIndex,
-            [FromQuery] PlayMethod playMethod,
+            [FromQuery] PlayMethod? playMethod,
             [FromQuery] string? liveStreamId,
             [FromQuery] string? liveStreamId,
-            [FromQuery] string playSessionId,
+            [FromQuery] string? playSessionId,
             [FromQuery] bool canSeek = false)
             [FromQuery] bool canSeek = false)
         {
         {
             var playbackStartInfo = new PlaybackStartInfo
             var playbackStartInfo = new PlaybackStartInfo
@@ -214,7 +214,7 @@ namespace Jellyfin.Api.Controllers
                 MediaSourceId = mediaSourceId,
                 MediaSourceId = mediaSourceId,
                 AudioStreamIndex = audioStreamIndex,
                 AudioStreamIndex = audioStreamIndex,
                 SubtitleStreamIndex = subtitleStreamIndex,
                 SubtitleStreamIndex = subtitleStreamIndex,
-                PlayMethod = playMethod,
+                PlayMethod = playMethod ?? PlayMethod.Transcode,
                 PlaySessionId = playSessionId,
                 PlaySessionId = playSessionId,
                 LiveStreamId = liveStreamId
                 LiveStreamId = liveStreamId
             };
             };
@@ -254,10 +254,10 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? subtitleStreamIndex,
             [FromQuery] int? subtitleStreamIndex,
             [FromQuery] int? volumeLevel,
             [FromQuery] int? volumeLevel,
-            [FromQuery] PlayMethod playMethod,
+            [FromQuery] PlayMethod? playMethod,
             [FromQuery] string? liveStreamId,
             [FromQuery] string? liveStreamId,
-            [FromQuery] string playSessionId,
-            [FromQuery] RepeatMode repeatMode,
+            [FromQuery] string? playSessionId,
+            [FromQuery] RepeatMode? repeatMode,
             [FromQuery] bool isPaused = false,
             [FromQuery] bool isPaused = false,
             [FromQuery] bool isMuted = false)
             [FromQuery] bool isMuted = false)
         {
         {
@@ -271,10 +271,10 @@ namespace Jellyfin.Api.Controllers
                 AudioStreamIndex = audioStreamIndex,
                 AudioStreamIndex = audioStreamIndex,
                 SubtitleStreamIndex = subtitleStreamIndex,
                 SubtitleStreamIndex = subtitleStreamIndex,
                 VolumeLevel = volumeLevel,
                 VolumeLevel = volumeLevel,
-                PlayMethod = playMethod,
+                PlayMethod = playMethod ?? PlayMethod.Transcode,
                 PlaySessionId = playSessionId,
                 PlaySessionId = playSessionId,
                 LiveStreamId = liveStreamId,
                 LiveStreamId = liveStreamId,
-                RepeatMode = repeatMode
+                RepeatMode = repeatMode ?? RepeatMode.RepeatNone
             };
             };
 
 
             playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
             playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
@@ -352,7 +352,7 @@ namespace Jellyfin.Api.Controllers
             return _userDataRepository.GetUserDataDto(item, user);
             return _userDataRepository.GetUserDataDto(item, user);
         }
         }
 
 
-        private PlayMethod ValidatePlayMethod(PlayMethod method, string playSessionId)
+        private PlayMethod ValidatePlayMethod(PlayMethod method, string? playSessionId)
         {
         {
             if (method == PlayMethod.Transcode)
             if (method == PlayMethod.Transcode)
             {
             {

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

@@ -45,7 +45,7 @@ namespace Jellyfin.Api.Controllers
         {
         {
             _installationManager = installationManager;
             _installationManager = installationManager;
             _pluginManager = pluginManager;
             _pluginManager = pluginManager;
-            _serializerOptions = JsonDefaults.GetOptions();
+            _serializerOptions = JsonDefaults.Options;
             _config = config;
             _config = config;
         }
         }
 
 

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

@@ -259,7 +259,8 @@ namespace Jellyfin.Api.Controllers
 
 
             var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
             var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
             Directory.CreateDirectory(fullCacheDirectory);
             Directory.CreateDirectory(fullCacheDirectory);
-            await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+            // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
+            await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
             await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
             await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
 
 
             var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
             var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));

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

@@ -112,7 +112,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? maxAudioSampleRate,
             [FromQuery] int? maxAudioSampleRate,
             [FromQuery] int? maxAudioBitDepth,
             [FromQuery] int? maxAudioBitDepth,
             [FromQuery] bool? enableRemoteMedia,
             [FromQuery] bool? enableRemoteMedia,
-            [FromQuery] bool breakOnNonKeyFrames,
+            [FromQuery] bool breakOnNonKeyFrames = false,
             [FromQuery] bool enableRedirection = true)
             [FromQuery] bool enableRedirection = true)
         {
         {
             var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
             var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);

+ 4 - 4
Jellyfin.Api/Controllers/VideoHlsController.cs

@@ -198,7 +198,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? height,
             [FromQuery] int? height,
             [FromQuery] int? videoBitRate,
             [FromQuery] int? videoBitRate,
             [FromQuery] int? subtitleStreamIndex,
             [FromQuery] int? subtitleStreamIndex,
-            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
             [FromQuery] int? maxRefFrames,
             [FromQuery] int? maxRefFrames,
             [FromQuery] int? maxVideoBitDepth,
             [FromQuery] int? maxVideoBitDepth,
             [FromQuery] bool? requireAvc,
             [FromQuery] bool? requireAvc,
@@ -213,7 +213,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? transcodeReasons,
             [FromQuery] string? transcodeReasons,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? videoStreamIndex,
             [FromQuery] int? videoStreamIndex,
-            [FromQuery] EncodingContext context,
+            [FromQuery] EncodingContext? context,
             [FromQuery] Dictionary<string, string> streamOptions,
             [FromQuery] Dictionary<string, string> streamOptions,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxHeight,
             [FromQuery] int? maxHeight,
@@ -253,7 +253,7 @@ namespace Jellyfin.Api.Controllers
                 Height = height,
                 Height = height,
                 VideoBitRate = videoBitRate,
                 VideoBitRate = videoBitRate,
                 SubtitleStreamIndex = subtitleStreamIndex,
                 SubtitleStreamIndex = subtitleStreamIndex,
-                SubtitleMethod = subtitleMethod,
+                SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
                 MaxRefFrames = maxRefFrames,
                 MaxRefFrames = maxRefFrames,
                 MaxVideoBitDepth = maxVideoBitDepth,
                 MaxVideoBitDepth = maxVideoBitDepth,
                 RequireAvc = requireAvc ?? true,
                 RequireAvc = requireAvc ?? true,
@@ -268,7 +268,7 @@ namespace Jellyfin.Api.Controllers
                 TranscodeReasons = transcodeReasons,
                 TranscodeReasons = transcodeReasons,
                 AudioStreamIndex = audioStreamIndex,
                 AudioStreamIndex = audioStreamIndex,
                 VideoStreamIndex = videoStreamIndex,
                 VideoStreamIndex = videoStreamIndex,
-                Context = context,
+                Context = context ?? EncodingContext.Streaming,
                 StreamOptions = streamOptions,
                 StreamOptions = streamOptions,
                 MaxHeight = maxHeight,
                 MaxHeight = maxHeight,
                 MaxWidth = maxWidth,
                 MaxWidth = maxWidth,

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

@@ -217,9 +217,7 @@ namespace Jellyfin.Api.Controllers
                 return BadRequest("Please supply at least two videos to merge.");
                 return BadRequest("Please supply at least two videos to merge.");
             }
             }
 
 
-            var videosWithVersions = items.Where(i => i.MediaSourceCount > 1).ToList();
-
-            var primaryVersion = videosWithVersions.FirstOrDefault();
+            var primaryVersion = items.FirstOrDefault(i => i.MediaSourceCount > 1 && string.IsNullOrEmpty(i.PrimaryVersionId));
             if (primaryVersion == null)
             if (primaryVersion == null)
             {
             {
                 primaryVersion = items
                 primaryVersion = items
@@ -364,7 +362,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? height,
             [FromQuery] int? height,
             [FromQuery] int? videoBitRate,
             [FromQuery] int? videoBitRate,
             [FromQuery] int? subtitleStreamIndex,
             [FromQuery] int? subtitleStreamIndex,
-            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
             [FromQuery] int? maxRefFrames,
             [FromQuery] int? maxRefFrames,
             [FromQuery] int? maxVideoBitDepth,
             [FromQuery] int? maxVideoBitDepth,
             [FromQuery] bool? requireAvc,
             [FromQuery] bool? requireAvc,
@@ -379,7 +377,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? transcodeReasons,
             [FromQuery] string? transcodeReasons,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? videoStreamIndex,
             [FromQuery] int? videoStreamIndex,
-            [FromQuery] EncodingContext context,
+            [FromQuery] EncodingContext? context,
             [FromQuery] Dictionary<string, string> streamOptions)
             [FromQuery] Dictionary<string, string> streamOptions)
         {
         {
             var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
             var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
@@ -418,7 +416,7 @@ namespace Jellyfin.Api.Controllers
                 Height = height,
                 Height = height,
                 VideoBitRate = videoBitRate,
                 VideoBitRate = videoBitRate,
                 SubtitleStreamIndex = subtitleStreamIndex,
                 SubtitleStreamIndex = subtitleStreamIndex,
-                SubtitleMethod = subtitleMethod,
+                SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
                 MaxRefFrames = maxRefFrames,
                 MaxRefFrames = maxRefFrames,
                 MaxVideoBitDepth = maxVideoBitDepth,
                 MaxVideoBitDepth = maxVideoBitDepth,
                 RequireAvc = requireAvc ?? true,
                 RequireAvc = requireAvc ?? true,
@@ -433,7 +431,7 @@ namespace Jellyfin.Api.Controllers
                 TranscodeReasons = transcodeReasons,
                 TranscodeReasons = transcodeReasons,
                 AudioStreamIndex = audioStreamIndex,
                 AudioStreamIndex = audioStreamIndex,
                 VideoStreamIndex = videoStreamIndex,
                 VideoStreamIndex = videoStreamIndex,
-                Context = context,
+                Context = context ?? EncodingContext.Streaming,
                 StreamOptions = streamOptions
                 StreamOptions = streamOptions
             };
             };
 
 
@@ -620,7 +618,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? height,
             [FromQuery] int? height,
             [FromQuery] int? videoBitRate,
             [FromQuery] int? videoBitRate,
             [FromQuery] int? subtitleStreamIndex,
             [FromQuery] int? subtitleStreamIndex,
-            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
             [FromQuery] int? maxRefFrames,
             [FromQuery] int? maxRefFrames,
             [FromQuery] int? maxVideoBitDepth,
             [FromQuery] int? maxVideoBitDepth,
             [FromQuery] bool? requireAvc,
             [FromQuery] bool? requireAvc,
@@ -635,7 +633,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] string? transcodeReasons,
             [FromQuery] string? transcodeReasons,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? audioStreamIndex,
             [FromQuery] int? videoStreamIndex,
             [FromQuery] int? videoStreamIndex,
-            [FromQuery] EncodingContext context,
+            [FromQuery] EncodingContext? context,
             [FromQuery] Dictionary<string, string> streamOptions)
             [FromQuery] Dictionary<string, string> streamOptions)
         {
         {
             return GetVideoStream(
             return GetVideoStream(

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

@@ -107,7 +107,8 @@ namespace Jellyfin.Api.Helpers
             // Headers only
             // Headers only
             if (isHeadRequest)
             if (isHeadRequest)
             {
             {
-                return new FileContentResult(Array.Empty<byte>(), contentType);
+                httpContext.Response.Headers[HeaderNames.ContentType] = contentType;
+                return new OkResult();
             }
             }
 
 
             var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);
             var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);

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

@@ -508,17 +508,15 @@ namespace Jellyfin.Api.Helpers
 
 
         private static void ApplyDeviceProfileSettings(StreamState state, IDlnaManager dlnaManager, IDeviceManager deviceManager, HttpRequest request, string? deviceProfileId, bool? @static)
         private static void ApplyDeviceProfileSettings(StreamState state, IDlnaManager dlnaManager, IDeviceManager deviceManager, HttpRequest request, string? deviceProfileId, bool? @static)
         {
         {
-            var headers = request.Headers;
-
             if (!string.IsNullOrWhiteSpace(deviceProfileId))
             if (!string.IsNullOrWhiteSpace(deviceProfileId))
             {
             {
                 state.DeviceProfile = dlnaManager.GetProfile(deviceProfileId);
                 state.DeviceProfile = dlnaManager.GetProfile(deviceProfileId);
-            }
-            else if (!string.IsNullOrWhiteSpace(deviceProfileId))
-            {
-                var caps = deviceManager.GetCapabilities(deviceProfileId);
 
 
-                state.DeviceProfile = caps == null ? dlnaManager.GetProfile(headers) : caps.DeviceProfile;
+                if (state.DeviceProfile == null)
+                {
+                    var caps = deviceManager.GetCapabilities(deviceProfileId);
+                    state.DeviceProfile = caps == null ? dlnaManager.GetProfile(request.Headers) : caps.DeviceProfile;
+                }
             }
             }
 
 
             var profile = state.DeviceProfile;
             var profile = state.DeviceProfile;

+ 0 - 1
Jellyfin.Api/Jellyfin.Api.csproj

@@ -28,7 +28,6 @@
 
 
   <!-- Code Analyzers-->
   <!-- Code Analyzers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />

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

@@ -1,4 +1,7 @@
-namespace Jellyfin.Api.Models.LibraryDtos
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Api.Models.LibraryDtos
 {
 {
     /// <summary>
     /// <summary>
     /// Media Update Info Dto.
     /// Media Update Info Dto.
@@ -6,14 +9,8 @@
     public class MediaUpdateInfoDto
     public class MediaUpdateInfoDto
     {
     {
         /// <summary>
         /// <summary>
-        /// Gets or sets media path.
-        /// </summary>
-        public string? Path { get; set; }
-
-        /// <summary>
-        /// Gets or sets media update type.
-        /// Created, Modified, Deleted.
+        /// Gets or sets the list of updates.
         /// </summary>
         /// </summary>
-        public string? UpdateType { get; set; }
+        public IReadOnlyList<MediaUpdateInfoPathDto> Updates { get; set; } = Array.Empty<MediaUpdateInfoPathDto>();
     }
     }
 }
 }

+ 19 - 0
Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoPathDto.cs

@@ -0,0 +1,19 @@
+namespace Jellyfin.Api.Models.LibraryDtos
+{
+    /// <summary>
+    /// The media update info path.
+    /// </summary>
+    public class MediaUpdateInfoPathDto
+    {
+        /// <summary>
+        /// Gets or sets media path.
+        /// </summary>
+        public string? Path { get; set; }
+
+        /// <summary>
+        /// Gets or sets media update type.
+        /// Created, Modified, Deleted.
+        /// </summary>
+        public string? UpdateType { get; set; }
+    }
+}

+ 23 - 0
Jellyfin.Api/Models/LibraryStructureDto/UpdateMediaPathRequestDto.cs

@@ -0,0 +1,23 @@
+using System.ComponentModel.DataAnnotations;
+using MediaBrowser.Model.Configuration;
+
+namespace Jellyfin.Api.Models.LibraryStructureDto
+{
+    /// <summary>
+    /// Update library options dto.
+    /// </summary>
+    public class UpdateMediaPathRequestDto
+    {
+        /// <summary>
+        /// Gets or sets the library name.
+        /// </summary>
+        [Required]
+        public string Name { get; set; } = null!;
+
+        /// <summary>
+        /// Gets or sets library folder path information.
+        /// </summary>
+        [Required]
+        public MediaPathInfo PathInfo { get; set; } = null!;
+    }
+}

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

@@ -1,6 +1,5 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
 using System.Text.Json.Serialization;
 using System.Text.Json.Serialization;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Json.Converters;
 using MediaBrowser.Common.Json.Converters;

+ 30 - 0
Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs

@@ -0,0 +1,30 @@
+using MediaBrowser.Model.Notifications;
+
+namespace Jellyfin.Api.Models.NotificationDtos
+{
+    /// <summary>
+    /// The admin notification dto.
+    /// </summary>
+    public class AdminNotificationDto
+    {
+        /// <summary>
+        /// Gets or sets the notification name.
+        /// </summary>
+        public string? Name { get; set; }
+
+        /// <summary>
+        /// Gets or sets the notification description.
+        /// </summary>
+        public string? Description { get; set; }
+
+        /// <summary>
+        /// Gets or sets the notification level.
+        /// </summary>
+        public NotificationLevel? NotificationLevel { get; set; }
+
+        /// <summary>
+        /// Gets or sets the notification url.
+        /// </summary>
+        public string? Url { get; set; }
+    }
+}

+ 7 - 53
Jellyfin.Data/DayOfWeekHelper.cs

@@ -1,67 +1,21 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
-using System.Collections.Generic;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
 
 
 namespace Jellyfin.Data
 namespace Jellyfin.Data
 {
 {
     public static class DayOfWeekHelper
     public static class DayOfWeekHelper
     {
     {
-        public static List<DayOfWeek> GetDaysOfWeek(DynamicDayOfWeek day)
+        public static DayOfWeek[] GetDaysOfWeek(DynamicDayOfWeek day)
         {
         {
-            var days = new List<DayOfWeek>(7);
-
-            if (day == DynamicDayOfWeek.Sunday
-                || day == DynamicDayOfWeek.Weekend
-                || day == DynamicDayOfWeek.Everyday)
-            {
-                days.Add(DayOfWeek.Sunday);
-            }
-
-            if (day == DynamicDayOfWeek.Monday
-                || day == DynamicDayOfWeek.Weekday
-                || day == DynamicDayOfWeek.Everyday)
-            {
-                days.Add(DayOfWeek.Monday);
-            }
-
-            if (day == DynamicDayOfWeek.Tuesday
-                || day == DynamicDayOfWeek.Weekday
-                || day == DynamicDayOfWeek.Everyday)
-            {
-                days.Add(DayOfWeek.Tuesday);
-            }
-
-            if (day == DynamicDayOfWeek.Wednesday
-                || day == DynamicDayOfWeek.Weekday
-                || day == DynamicDayOfWeek.Everyday)
+            return day switch
             {
             {
-                days.Add(DayOfWeek.Wednesday);
-            }
-
-            if (day == DynamicDayOfWeek.Thursday
-                || day == DynamicDayOfWeek.Weekday
-                || day == DynamicDayOfWeek.Everyday)
-            {
-                days.Add(DayOfWeek.Thursday);
-            }
-
-            if (day == DynamicDayOfWeek.Friday
-                || day == DynamicDayOfWeek.Weekday
-                || day == DynamicDayOfWeek.Everyday)
-            {
-                days.Add(DayOfWeek.Friday);
-            }
-
-            if (day == DynamicDayOfWeek.Saturday
-                || day == DynamicDayOfWeek.Weekend
-                || day == DynamicDayOfWeek.Everyday)
-            {
-                days.Add(DayOfWeek.Saturday);
-            }
-
-            return days;
+                DynamicDayOfWeek.Everyday => new[] { DayOfWeek.Sunday, DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday },
+                DynamicDayOfWeek.Weekday => new[] { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday },
+                DynamicDayOfWeek.Weekend => new[] { DayOfWeek.Sunday, DayOfWeek.Saturday },
+                _ => new[] { (DayOfWeek)day }
+            };
         }
         }
     }
     }
 }
 }

+ 1 - 0
Jellyfin.Data/Entities/Libraries/Collection.cs

@@ -1,3 +1,4 @@
+#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
 #pragma warning disable CA2227
 #pragma warning disable CA2227
 
 
 using System.Collections.Generic;
 using System.Collections.Generic;

+ 2 - 0
Jellyfin.Data/Entities/Libraries/MediaFileStream.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
+
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
 using System.ComponentModel.DataAnnotations.Schema;
 using Jellyfin.Data.Interfaces;
 using Jellyfin.Data.Interfaces;

+ 2 - 0
Jellyfin.Data/Entities/Permission.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
+
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
 using System.ComponentModel.DataAnnotations.Schema;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;

+ 2 - 5
Jellyfin.Data/Jellyfin.Data.csproj

@@ -5,6 +5,8 @@
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+    <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
     <Nullable>enable</Nullable>
     <Nullable>enable</Nullable>
     <PublishRepositoryUrl>true</PublishRepositoryUrl>
     <PublishRepositoryUrl>true</PublishRepositoryUrl>
     <EmbedUntrackedSources>true</EmbedUntrackedSources>
     <EmbedUntrackedSources>true</EmbedUntrackedSources>
@@ -25,17 +27,12 @@
     <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
     <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
   </PropertyGroup>
   </PropertyGroup>
 
 
-  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
   <ItemGroup>
   <ItemGroup>
     <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
     <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
   </ItemGroup>
   </ItemGroup>
 
 
   <!-- Code analysers-->
   <!-- Code analysers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />

+ 7 - 5
Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj

@@ -11,6 +11,8 @@
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <Nullable>enable</Nullable>
     <Nullable>enable</Nullable>
+    <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
@@ -30,16 +32,16 @@
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
   </ItemGroup>
   </ItemGroup>
 
 
+  <ItemGroup>
+    <!-- Needed for https://github.com/dotnet/roslyn-analyzers/issues/4382 which is in the SDK yet -->
+    <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3" PrivateAssets="All" />
+  </ItemGroup>
+
   <!-- Code analysers-->
   <!-- Code analysers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
   </ItemGroup>
   </ItemGroup>
 
 
-  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
 </Project>
 </Project>

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

@@ -274,8 +274,8 @@ namespace Jellyfin.Drawing.Skia
 
 
             if (requiresTransparencyHack || forceCleanBitmap)
             if (requiresTransparencyHack || forceCleanBitmap)
             {
             {
-                using var codec = SKCodec.Create(NormalizePath(path));
-                if (codec == null)
+                using SKCodec codec = SKCodec.Create(NormalizePath(path), out SKCodecResult res);
+                if (res != SKCodecResult.Success)
                 {
                 {
                     origin = GetSKEncodedOrigin(orientation);
                     origin = GetSKEncodedOrigin(orientation);
                     return null;
                     return null;
@@ -345,11 +345,6 @@ namespace Jellyfin.Drawing.Skia
 
 
         private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
         private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
         {
         {
-            if (origin == SKEncodedOrigin.Default)
-            {
-                return bitmap;
-            }
-
             var needsFlip = origin == SKEncodedOrigin.LeftBottom
             var needsFlip = origin == SKEncodedOrigin.LeftBottom
                             || origin == SKEncodedOrigin.LeftTop
                             || origin == SKEncodedOrigin.LeftTop
                             || origin == SKEncodedOrigin.RightBottom
                             || origin == SKEncodedOrigin.RightBottom
@@ -447,7 +442,7 @@ namespace Jellyfin.Drawing.Skia
         }
         }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
-        public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
+        public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat)
         {
         {
             if (inputPath.Length == 0)
             if (inputPath.Length == 0)
             {
             {
@@ -459,7 +454,7 @@ namespace Jellyfin.Drawing.Skia
                 throw new ArgumentException("String can't be empty.", nameof(outputPath));
                 throw new ArgumentException("String can't be empty.", nameof(outputPath));
             }
             }
 
 
-            var skiaOutputFormat = GetImageFormat(selectedOutputFormat);
+            var skiaOutputFormat = GetImageFormat(outputFormat);
 
 
             var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
             var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
             var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer);
             var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer);

+ 2 - 5
Jellyfin.Networking/Jellyfin.Networking.csproj

@@ -5,6 +5,8 @@
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <Nullable>enable</Nullable>
     <Nullable>enable</Nullable>
+    <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
@@ -13,16 +15,11 @@
 
 
   <!-- Code Analyzers-->
   <!-- Code Analyzers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
   </ItemGroup>
   </ItemGroup>
 
 
-  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
   <ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
     <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
     <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />

+ 16 - 5
Jellyfin.Networking/Manager/NetworkManager.cs

@@ -285,14 +285,25 @@ namespace Jellyfin.Networking.Manager
                 // No bind address and no exclusions, so listen on all interfaces.
                 // No bind address and no exclusions, so listen on all interfaces.
                 Collection<IPObject> result = new Collection<IPObject>();
                 Collection<IPObject> result = new Collection<IPObject>();
 
 
-                if (IsIP4Enabled)
+                if (IsIP6Enabled && IsIP4Enabled)
+                {
+                    // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any
+                    result.AddItem(IPAddress.IPv6Any);
+                }
+                else if (IsIP4Enabled)
                 {
                 {
                     result.AddItem(IPAddress.Any);
                     result.AddItem(IPAddress.Any);
                 }
                 }
-
-                if (IsIP6Enabled)
+                else if (IsIP6Enabled)
                 {
                 {
-                    result.AddItem(IPAddress.IPv6Any);
+                    // Cannot use IPv6Any as Kestrel will bind to IPv4 addresses.
+                    foreach (var iface in _interfaceAddresses)
+                    {
+                        if (iface.AddressFamily == AddressFamily.InterNetworkV6)
+                        {
+                            result.AddItem(iface.Address);
+                        }
+                    }
                 }
                 }
 
 
                 return result;
                 return result;
@@ -414,7 +425,7 @@ namespace Jellyfin.Networking.Manager
             }
             }
 
 
             // There isn't any others, so we'll use the loopback.
             // There isn't any others, so we'll use the loopback.
-            result = IsIP6Enabled ? "::" : "127.0.0.1";
+            result = IsIP6Enabled ? "::1" : "127.0.0.1";
             _logger.LogWarning("{Source}: GetBindInterface: Loopback {Result} returned.", source, result);
             _logger.LogWarning("{Source}: GetBindInterface: Loopback {Result} returned.", source, result);
             return result;
             return result;
         }
         }

+ 4 - 4
Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs

@@ -29,20 +29,20 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public async Task OnEvent(GenericEventArgs<AuthenticationResult> e)
+        public async Task OnEvent(GenericEventArgs<AuthenticationResult> eventArgs)
         {
         {
             await _activityManager.CreateAsync(new ActivityLog(
             await _activityManager.CreateAsync(new ActivityLog(
                 string.Format(
                 string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localizationManager.GetLocalizedString("AuthenticationSucceededWithUserName"),
                     _localizationManager.GetLocalizedString("AuthenticationSucceededWithUserName"),
-                    e.Argument.User.Name),
+                    eventArgs.Argument.User.Name),
                 "AuthenticationSucceeded",
                 "AuthenticationSucceeded",
-                e.Argument.User.Id)
+                eventArgs.Argument.User.Id)
             {
             {
                 ShortOverview = string.Format(
                 ShortOverview = string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
                     _localizationManager.GetLocalizedString("LabelIpAddressValue"),
                     _localizationManager.GetLocalizedString("LabelIpAddressValue"),
-                    e.Argument.SessionInfo.RemoteEndPoint),
+                    eventArgs.Argument.SessionInfo.RemoteEndPoint),
             }).ConfigureAwait(false);
             }).ConfigureAwait(false);
         }
         }
     }
     }

+ 7 - 7
Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs

@@ -33,10 +33,10 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.System
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public async Task OnEvent(TaskCompletionEventArgs e)
+        public async Task OnEvent(TaskCompletionEventArgs eventArgs)
         {
         {
-            var result = e.Result;
-            var task = e.Task;
+            var result = eventArgs.Result;
+            var task = eventArgs.Task;
 
 
             if (task.ScheduledTask is IConfigurableScheduledTask activityTask
             if (task.ScheduledTask is IConfigurableScheduledTask activityTask
                 && !activityTask.IsLogged)
                 && !activityTask.IsLogged)
@@ -54,14 +54,14 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.System
             {
             {
                 var vals = new List<string>();
                 var vals = new List<string>();
 
 
-                if (!string.IsNullOrEmpty(e.Result.ErrorMessage))
+                if (!string.IsNullOrEmpty(eventArgs.Result.ErrorMessage))
                 {
                 {
-                    vals.Add(e.Result.ErrorMessage);
+                    vals.Add(eventArgs.Result.ErrorMessage);
                 }
                 }
 
 
-                if (!string.IsNullOrEmpty(e.Result.LongErrorMessage))
+                if (!string.IsNullOrEmpty(eventArgs.Result.LongErrorMessage))
                 {
                 {
-                    vals.Add(e.Result.LongErrorMessage);
+                    vals.Add(eventArgs.Result.LongErrorMessage);
                 }
                 }
 
 
                 await _activityManager.CreateAsync(new ActivityLog(
                 await _activityManager.CreateAsync(new ActivityLog(

+ 2 - 2
Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs

@@ -30,13 +30,13 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public async Task OnEvent(PluginUninstalledEventArgs e)
+        public async Task OnEvent(PluginUninstalledEventArgs eventArgs)
         {
         {
             await _activityManager.CreateAsync(new ActivityLog(
             await _activityManager.CreateAsync(new ActivityLog(
                     string.Format(
                     string.Format(
                         CultureInfo.InvariantCulture,
                         CultureInfo.InvariantCulture,
                         _localizationManager.GetLocalizedString("PluginUninstalledWithName"),
                         _localizationManager.GetLocalizedString("PluginUninstalledWithName"),
-                        e.Argument.Name),
+                        eventArgs.Argument.Name),
                     NotificationType.PluginUninstalled.ToString(),
                     NotificationType.PluginUninstalled.ToString(),
                     Guid.Empty))
                     Guid.Empty))
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);

+ 3 - 3
Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs

@@ -30,12 +30,12 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Users
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public async Task OnEvent(UserUpdatedEventArgs e)
+        public async Task OnEvent(UserUpdatedEventArgs eventArgs)
         {
         {
             await _sessionManager.SendMessageToUserSessions(
             await _sessionManager.SendMessageToUserSessions(
-                new List<Guid> { e.Argument.Id },
+                new List<Guid> { eventArgs.Argument.Id },
                 SessionMessageType.UserUpdated,
                 SessionMessageType.UserUpdated,
-                _userManager.GetUserDto(e.Argument),
+                _userManager.GetUserDto(eventArgs.Argument),
                 CancellationToken.None).ConfigureAwait(false);
                 CancellationToken.None).ConfigureAwait(false);
         }
         }
     }
     }

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

@@ -6,6 +6,8 @@
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <Nullable>enable</Nullable>
     <Nullable>enable</Nullable>
+    <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
   <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
@@ -14,7 +16,6 @@
 
 
   <!-- Code analysers-->
   <!-- Code analysers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />

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

@@ -66,7 +66,7 @@ namespace Jellyfin.Server.Implementations.Users
                 else if (string.Equals(
                 else if (string.Equals(
                     spr.Pin.Replace("-", string.Empty, StringComparison.Ordinal),
                     spr.Pin.Replace("-", string.Empty, StringComparison.Ordinal),
                     pin.Replace("-", string.Empty, StringComparison.Ordinal),
                     pin.Replace("-", string.Empty, StringComparison.Ordinal),
-                    StringComparison.InvariantCultureIgnoreCase))
+                    StringComparison.OrdinalIgnoreCase))
                 {
                 {
                     var resetUser = userManager.GetUserByName(spr.UserName)
                     var resetUser = userManager.GetUserByName(spr.UserName)
                         ?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found");
                         ?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found");

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

@@ -1,4 +1,5 @@
 #pragma warning disable CA1307
 #pragma warning disable CA1307
+#pragma warning disable CA1309
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
@@ -35,7 +36,7 @@ namespace Jellyfin.Server.Implementations.Users
 
 
             if (prefs == null)
             if (prefs == null)
             {
             {
-                prefs = new DisplayPreferences(userId,  itemId, client);
+                prefs = new DisplayPreferences(userId, itemId, client);
                 _dbContext.DisplayPreferences.Add(prefs);
                 _dbContext.DisplayPreferences.Add(prefs);
             }
             }
 
 

+ 4 - 0
Jellyfin.Server/CoreAppHost.cs

@@ -20,6 +20,7 @@ using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Activity;
 using MediaBrowser.Model.Activity;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
@@ -36,18 +37,21 @@ namespace Jellyfin.Server
         /// <param name="applicationPaths">The <see cref="ServerApplicationPaths" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="applicationPaths">The <see cref="ServerApplicationPaths" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param>
+        /// <param name="startupConfig">The <see cref="IConfiguration" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="collection">The <see cref="IServiceCollection"/> to be used by the <see cref="CoreAppHost"/>.</param>
         /// <param name="collection">The <see cref="IServiceCollection"/> to be used by the <see cref="CoreAppHost"/>.</param>
         public CoreAppHost(
         public CoreAppHost(
             IServerApplicationPaths applicationPaths,
             IServerApplicationPaths applicationPaths,
             ILoggerFactory loggerFactory,
             ILoggerFactory loggerFactory,
             IStartupOptions options,
             IStartupOptions options,
+            IConfiguration startupConfig,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
             IServiceCollection collection)
             IServiceCollection collection)
             : base(
             : base(
                 applicationPaths,
                 applicationPaths,
                 loggerFactory,
                 loggerFactory,
                 options,
                 options,
+                startupConfig,
                 fileSystem,
                 fileSystem,
                 collection)
                 collection)
         {
         {

+ 1 - 2
Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs

@@ -225,14 +225,13 @@ namespace Jellyfin.Server.Extensions
                 .AddJsonOptions(options =>
                 .AddJsonOptions(options =>
                 {
                 {
                     // Update all properties that are set in JsonDefaults
                     // Update all properties that are set in JsonDefaults
-                    var jsonOptions = JsonDefaults.GetPascalCaseOptions();
+                    var jsonOptions = JsonDefaults.PascalCaseOptions;
 
 
                     // From JsonDefaults
                     // From JsonDefaults
                     options.JsonSerializerOptions.ReadCommentHandling = jsonOptions.ReadCommentHandling;
                     options.JsonSerializerOptions.ReadCommentHandling = jsonOptions.ReadCommentHandling;
                     options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented;
                     options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented;
                     options.JsonSerializerOptions.DefaultIgnoreCondition = jsonOptions.DefaultIgnoreCondition;
                     options.JsonSerializerOptions.DefaultIgnoreCondition = jsonOptions.DefaultIgnoreCondition;
                     options.JsonSerializerOptions.NumberHandling = jsonOptions.NumberHandling;
                     options.JsonSerializerOptions.NumberHandling = jsonOptions.NumberHandling;
-                    options.JsonSerializerOptions.PropertyNameCaseInsensitive = jsonOptions.PropertyNameCaseInsensitive;
 
 
                     options.JsonSerializerOptions.Converters.Clear();
                     options.JsonSerializerOptions.Converters.Clear();
                     foreach (var converter in jsonOptions.Converters)
                     foreach (var converter in jsonOptions.Converters)

+ 2 - 0
Jellyfin.Server/Filters/WebsocketModelFilter.cs

@@ -25,6 +25,8 @@ namespace Jellyfin.Server.Filters
             context.SchemaGenerator.GenerateSchema(typeof(GeneralCommandType), context.SchemaRepository);
             context.SchemaGenerator.GenerateSchema(typeof(GeneralCommandType), context.SchemaRepository);
 
 
             context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<object>), context.SchemaRepository);
             context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<object>), context.SchemaRepository);
+
+            context.SchemaGenerator.GenerateSchema(typeof(SessionMessageType), context.SchemaRepository);
         }
         }
     }
     }
 }
 }

+ 1 - 1
Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs

@@ -12,7 +12,7 @@ namespace Jellyfin.Server.Formatters
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="CamelCaseJsonProfileFormatter"/> class.
         /// Initializes a new instance of the <see cref="CamelCaseJsonProfileFormatter"/> class.
         /// </summary>
         /// </summary>
-        public CamelCaseJsonProfileFormatter() : base(JsonDefaults.GetCamelCaseOptions())
+        public CamelCaseJsonProfileFormatter() : base(JsonDefaults.CamelCaseOptions)
         {
         {
             SupportedMediaTypes.Clear();
             SupportedMediaTypes.Clear();
             SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(JsonDefaults.CamelCaseMediaType));
             SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(JsonDefaults.CamelCaseMediaType));

+ 1 - 1
Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs

@@ -13,7 +13,7 @@ namespace Jellyfin.Server.Formatters
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="PascalCaseJsonProfileFormatter"/> class.
         /// Initializes a new instance of the <see cref="PascalCaseJsonProfileFormatter"/> class.
         /// </summary>
         /// </summary>
-        public PascalCaseJsonProfileFormatter() : base(JsonDefaults.GetPascalCaseOptions())
+        public PascalCaseJsonProfileFormatter() : base(JsonDefaults.PascalCaseOptions)
         {
         {
             SupportedMediaTypes.Clear();
             SupportedMediaTypes.Clear();
             // Add application/json for default formatter
             // Add application/json for default formatter

+ 3 - 6
Jellyfin.Server/Jellyfin.Server.csproj

@@ -13,7 +13,9 @@
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <Nullable>enable</Nullable>
     <Nullable>enable</Nullable>
-    <DisableImplicitAspNetCoreAnalyzers>true</DisableImplicitAspNetCoreAnalyzers>
+    <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
+    <!-- <DisableImplicitAspNetCoreAnalyzers>true</DisableImplicitAspNetCoreAnalyzers> -->
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
@@ -26,16 +28,11 @@
 
 
   <!-- Code Analyzers-->
   <!-- Code Analyzers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
   </ItemGroup>
   </ItemGroup>
 
 
-  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
   <ItemGroup>
   <ItemGroup>
     <PackageReference Include="CommandLineParser" Version="2.8.0" />
     <PackageReference Include="CommandLineParser" Version="2.8.0" />
     <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
     <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />

+ 3 - 0
Jellyfin.Server/Migrations/MigrationOptions.cs

@@ -16,9 +16,12 @@ namespace Jellyfin.Server.Migrations
             Applied = new List<(Guid Id, string Name)>();
             Applied = new List<(Guid Id, string Name)>();
         }
         }
 
 
+// .Net xml serializer can't handle interfaces
+#pragma warning disable CA1002 // Do not expose generic lists
         /// <summary>
         /// <summary>
         /// Gets the list of applied migration routine names.
         /// Gets the list of applied migration routine names.
         /// </summary>
         /// </summary>
         public List<(Guid Id, string Name)> Applied { get; }
         public List<(Guid Id, string Name)> Applied { get; }
+#pragma warning restore CA1002
     }
     }
 }
 }

+ 1 - 1
Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs

@@ -74,7 +74,7 @@ namespace Jellyfin.Server.Migrations.Routines
 
 
                 foreach (var entry in queryResult)
                 foreach (var entry in queryResult)
                 {
                 {
-                    UserMockup? mockup = JsonSerializer.Deserialize<UserMockup>(entry[2].ToBlob(), JsonDefaults.GetOptions());
+                    UserMockup? mockup = JsonSerializer.Deserialize<UserMockup>(entry[2].ToBlob(), JsonDefaults.Options);
                     if (mockup == null)
                     if (mockup == null)
                     {
                     {
                         continue;
                         continue;

+ 6 - 5
Jellyfin.Server/Program.cs

@@ -164,6 +164,7 @@ namespace Jellyfin.Server
                 appPaths,
                 appPaths,
                 _loggerFactory,
                 _loggerFactory,
                 options,
                 options,
+                startupConfig,
                 new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
                 new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
                 serviceCollection);
                 serviceCollection);
 
 
@@ -198,11 +199,11 @@ namespace Jellyfin.Server
                 }
                 }
                 catch
                 catch
                 {
                 {
-                    _logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in system.xml and try again.");
+                    _logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again.");
                     throw;
                     throw;
                 }
                 }
 
 
-                await appHost.RunStartupTasksAsync().ConfigureAwait(false);
+                await appHost.RunStartupTasksAsync(_tokenSource.Token).ConfigureAwait(false);
 
 
                 stopWatch.Stop();
                 stopWatch.Stop();
 
 
@@ -221,7 +222,7 @@ namespace Jellyfin.Server
             }
             }
             finally
             finally
             {
             {
-                appHost?.Dispose();
+                appHost.Dispose();
             }
             }
 
 
             if (_restartOnShutdown)
             if (_restartOnShutdown)
@@ -280,7 +281,7 @@ namespace Jellyfin.Server
                     bool flagged = false;
                     bool flagged = false;
                     foreach (IPObject netAdd in addresses)
                     foreach (IPObject netAdd in addresses)
                     {
                     {
-                        _logger.LogInformation("Kestrel listening on {0}", netAdd);
+                        _logger.LogInformation("Kestrel listening on {Address}", netAdd.Address == IPAddress.IPv6Any ? "All Addresses" : netAdd);
                         options.Listen(netAdd.Address, appHost.HttpPort);
                         options.Listen(netAdd.Address, appHost.HttpPort);
                         if (appHost.ListenWithHttps)
                         if (appHost.ListenWithHttps)
                         {
                         {
@@ -622,7 +623,7 @@ namespace Jellyfin.Server
             string commandLineArgsString;
             string commandLineArgsString;
             if (options.RestartArgs != null)
             if (options.RestartArgs != null)
             {
             {
-                commandLineArgsString = options.RestartArgs ?? string.Empty;
+                commandLineArgsString = options.RestartArgs;
             }
             }
             else
             else
             {
             {

+ 1 - 1
Jellyfin.Server/Properties/AssemblyInfo.cs

@@ -21,4 +21,4 @@ using System.Runtime.InteropServices;
 // COM, set the ComVisible attribute to true on that type.
 // COM, set the ComVisible attribute to true on that type.
 [assembly: ComVisible(false)]
 [assembly: ComVisible(false)]
 
 
-[assembly: InternalsVisibleTo("Jellyfin.Api.Tests")]
+[assembly: InternalsVisibleTo("Jellyfin.Server.Tests")]

+ 2 - 2
Jellyfin.Server/StartupOptions.cs

@@ -77,7 +77,7 @@ namespace Jellyfin.Server
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         [Option("published-server-url", Required = false, HelpText = "Jellyfin Server URL to publish via auto discover process")]
         [Option("published-server-url", Required = false, HelpText = "Jellyfin Server URL to publish via auto discover process")]
-        public Uri? PublishedServerUrl { get; set; }
+        public string? PublishedServerUrl { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets the command line options as a dictionary that can be used in the .NET configuration system.
         /// Gets the command line options as a dictionary that can be used in the .NET configuration system.
@@ -94,7 +94,7 @@ namespace Jellyfin.Server
 
 
             if (PublishedServerUrl != null)
             if (PublishedServerUrl != null)
             {
             {
-                config.Add(UdpServer.AddressOverrideConfigKey, PublishedServerUrl.ToString());
+                config.Add(UdpServer.AddressOverrideConfigKey, PublishedServerUrl);
             }
             }
 
 
             if (FFmpegPath != null)
             if (FFmpegPath != null)

+ 21 - 7
Jellyfin.sln

@@ -68,14 +68,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementat
 EndProject
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "Jellyfin.Networking\Jellyfin.Networking.csproj", "{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}"
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "Jellyfin.Networking\Jellyfin.Networking.csproj", "{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}"
 EndProject
 EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking.Tests", "tests\Jellyfin.Networking.Tests\NetworkTesting\Jellyfin.Networking.Tests.csproj", "{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}"
-EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Dlna.Tests", "tests\Jellyfin.Dlna.Tests\Jellyfin.Dlna.Tests.csproj", "{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}"
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Dlna.Tests", "tests\Jellyfin.Dlna.Tests\Jellyfin.Dlna.Tests.csproj", "{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}"
 EndProject
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.XbmcMetadata.Tests", "tests\Jellyfin.XbmcMetadata.Tests\Jellyfin.XbmcMetadata.Tests.csproj", "{30922383-D513-4F4D-B890-A940B57FA353}"
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.XbmcMetadata.Tests", "tests\Jellyfin.XbmcMetadata.Tests\Jellyfin.XbmcMetadata.Tests.csproj", "{30922383-D513-4F4D-B890-A940B57FA353}"
 EndProject
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Model.Tests", "tests\Jellyfin.Model.Tests\Jellyfin.Model.Tests.csproj", "{FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}"
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Model.Tests", "tests\Jellyfin.Model.Tests\Jellyfin.Model.Tests.csproj", "{FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}"
 EndProject
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Networking.Tests", "tests\Jellyfin.Networking.Tests\Jellyfin.Networking.Tests.csproj", "{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Tests", "tests\Jellyfin.Server.Tests\Jellyfin.Server.Tests.csproj", "{3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Integration.Tests", "tests\Jellyfin.Server.Integration.Tests\Jellyfin.Server.Integration.Tests.csproj", "{68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}"
+EndProject
 Global
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
 		Debug|Any CPU = Debug|Any CPU
@@ -190,10 +194,6 @@ Global
 		{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.Build.0 = Release|Any CPU
 		{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.Build.0 = Release|Any CPU
-		{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.Build.0 = Release|Any CPU
 		{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -206,6 +206,18 @@ Global
 		{FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}.Release|Any CPU.Build.0 = Release|Any CPU
 		{FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}.Release|Any CPU.Build.0 = Release|Any CPU
+		{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.Build.0 = Release|Any CPU
+		{3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9}.Release|Any CPU.Build.0 = Release|Any CPU
+		{68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
 		HideSolutionNode = FALSE
@@ -217,10 +229,12 @@ Global
 		{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
-		{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{30922383-D513-4F4D-B890-A940B57FA353} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{30922383-D513-4F4D-B890-A940B57FA353} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
+		{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
+		{3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
+		{68B0B823-A5AC-4E8B-82EA-965AAC7BF76E} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 	EndGlobalSection
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}
 		SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}

+ 89 - 0
MediaBrowser.Common/Crc32.cs

@@ -0,0 +1,89 @@
+#pragma warning disable CS1591
+
+using System;
+
+namespace MediaBrowser.Common
+{
+    public static class Crc32
+    {
+        private static readonly uint[] _crcTable =
+        {
+            0x00000000, 0x77073096, 0xee0e612c, 0x990951ba,
+            0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
+            0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
+            0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
+            0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de,
+            0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
+            0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
+            0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
+            0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
+            0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
+            0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940,
+            0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
+            0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116,
+            0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
+            0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
+            0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
+            0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a,
+            0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
+            0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818,
+            0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
+            0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
+            0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
+            0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c,
+            0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
+            0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2,
+            0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
+            0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
+            0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
+            0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086,
+            0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
+            0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4,
+            0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
+            0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
+            0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
+            0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
+            0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
+            0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe,
+            0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
+            0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
+            0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
+            0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252,
+            0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
+            0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60,
+            0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
+            0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
+            0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
+            0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04,
+            0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
+            0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
+            0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
+            0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
+            0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
+            0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e,
+            0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
+            0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c,
+            0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
+            0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
+            0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
+            0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0,
+            0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
+            0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6,
+            0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
+            0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
+            0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
+        };
+
+        public static uint Compute(ReadOnlySpan<byte> bytes)
+        {
+            var crc = 0xffffffff;
+            var len = bytes.Length;
+            for (var i = 0; i < len; i++)
+            {
+                crc = (crc >> 8) ^ _crcTable[(bytes[i] ^ crc) & 0xff];
+            }
+
+            return ~crc;
+        }
+    }
+}

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä