Przeglądaj źródła

Merge branch 'master' into fordiscussion

BaronGreenback 4 lat temu
rodzic
commit
2c5609b333
37 zmienionych plików z 284 dodań i 217 usunięć
  1. 6 0
      .ci/azure-pipelines-api-client.yml
  2. 3 0
      .npmrc
  3. 4 13
      Emby.Naming/AudioBook/AudioBookResolver.cs
  4. 1 4
      Emby.Server.Implementations/ApplicationHost.cs
  5. 4 4
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  6. 5 12
      Emby.Server.Implementations/HttpServer/WebSocketManager.cs
  7. 3 0
      Emby.Server.Implementations/Localization/Core/hi.json
  8. 1 1
      Emby.Server.Implementations/Localization/Core/sq.json
  9. 1 1
      Emby.Server.Implementations/Localization/Core/sv.json
  10. 2 2
      Jellyfin.Api/Jellyfin.Api.csproj
  11. 2 2
      Jellyfin.Data/Jellyfin.Data.csproj
  12. 2 2
      Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
  13. 11 0
      Jellyfin.Server/CoreAppHost.cs
  14. 4 4
      Jellyfin.Server/Jellyfin.Server.csproj
  15. 1 1
      Jellyfin.Server/Program.cs
  16. 2 2
      MediaBrowser.Common/MediaBrowser.Common.csproj
  17. 2 2
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  18. 140 102
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  19. 0 6
      MediaBrowser.Controller/Net/IWebSocketManager.cs
  20. 10 0
      MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
  21. 1 1
      MediaBrowser.Model/MediaBrowser.Model.csproj
  22. 5 2
      MediaBrowser.Model/System/PublicSystemInfo.cs
  23. 3 3
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  24. 43 32
      MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
  25. 2 2
      debian/postrm
  26. 1 1
      deployment/Dockerfile.debian.amd64
  27. 1 1
      deployment/Dockerfile.debian.arm64
  28. 1 1
      deployment/Dockerfile.debian.armhf
  29. 1 1
      deployment/Dockerfile.linux.amd64
  30. 1 1
      deployment/Dockerfile.macos
  31. 1 1
      deployment/Dockerfile.portable
  32. 1 1
      deployment/Dockerfile.ubuntu.amd64
  33. 1 1
      deployment/Dockerfile.ubuntu.arm64
  34. 1 1
      deployment/Dockerfile.ubuntu.armhf
  35. 1 1
      deployment/Dockerfile.windows.amd64
  36. 2 2
      tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
  37. 14 7
      tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs

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

@@ -28,6 +28,12 @@ jobs:
       inputs:
         script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar"
 
+## Authenticate with npm registry
+    - task: npmAuthenticate@0
+      inputs:
+        workingFile: ./.npmrc
+        customEndpoint: 'jellyfin-bot for NPM'
+
 ## Generate npm api client
 # Unstable
     - task: CmdLine@2

+ 3 - 0
.npmrc

@@ -0,0 +1,3 @@
+registry=https://registry.npmjs.org/
+@jellyfin:registry=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/
+always-auth=true

+ 4 - 13
Emby.Naming/AudioBook/AudioBookResolver.cs

@@ -1,3 +1,4 @@
+#nullable enable
 #pragma warning disable CS1591
 
 using System;
@@ -16,21 +17,11 @@ namespace Emby.Naming.AudioBook
             _options = options;
         }
 
-        public AudioBookFileInfo ParseFile(string path)
+        public AudioBookFileInfo? Resolve(string path, bool isDirectory = false)
         {
-            return Resolve(path, false);
-        }
-
-        public AudioBookFileInfo ParseDirectory(string path)
-        {
-            return Resolve(path, true);
-        }
-
-        public AudioBookFileInfo Resolve(string path, bool isDirectory = false)
-        {
-            if (string.IsNullOrEmpty(path))
+            if (path.Length == 0)
             {
-                throw new ArgumentNullException(nameof(path));
+                throw new ArgumentException("String can't be empty.", nameof(path));
             }
 
             // TODO

+ 1 - 4
Emby.Server.Implementations/ApplicationHost.cs

@@ -128,7 +128,6 @@ namespace Emby.Server.Implementations
         private IMediaEncoder _mediaEncoder;
         private ISessionManager _sessionManager;
         private IHttpClientFactory _httpClientFactory;
-        private IWebSocketManager _webSocketManager;
         private string[] _urlPrefixes;
 
         /// <summary>
@@ -653,7 +652,6 @@ namespace Emby.Server.Implementations
             _mediaEncoder = Resolve<IMediaEncoder>();
             _sessionManager = Resolve<ISessionManager>();
             _httpClientFactory = Resolve<IHttpClientFactory>();
-            _webSocketManager = Resolve<IWebSocketManager>();
 
             ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
 
@@ -788,7 +786,6 @@ namespace Emby.Server.Implementations
             }
 
             _urlPrefixes = GetUrlPrefixes().ToArray();
-            _webSocketManager.Init(GetExports<IWebSocketListener>());
 
             Resolve<ILibraryManager>().AddParts(
                 GetExports<IResolverIgnoreRule>(),
@@ -1068,7 +1065,7 @@ namespace Emby.Server.Implementations
                         }
                         else
                         {
-                            // Un-versioned folder - Add it under the path name and version 0.0.0.1.                        
+                            // Un-versioned folder - Add it under the path name and version 0.0.0.1.
                             versions.Add((new Version(0, 0, 0, 1), metafile, dir));
                         }
                     }

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

@@ -32,10 +32,10 @@
     <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.8" />
-    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.8" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
-    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.8" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.9" />
     <PackageReference Include="Mono.Nat" Version="3.0.0" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
     <PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />

+ 5 - 12
Emby.Server.Implementations/HttpServer/WebSocketManager.cs

@@ -2,7 +2,6 @@
 
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using System.Net.WebSockets;
 using System.Threading.Tasks;
 using Jellyfin.Data.Events;
@@ -14,16 +13,18 @@ namespace Emby.Server.Implementations.HttpServer
 {
     public class WebSocketManager : IWebSocketManager
     {
+        private readonly Lazy<IEnumerable<IWebSocketListener>> _webSocketListeners;
         private readonly ILogger<WebSocketManager> _logger;
         private readonly ILoggerFactory _loggerFactory;
 
-        private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
         private bool _disposed = false;
 
         public WebSocketManager(
+            Lazy<IEnumerable<IWebSocketListener>> webSocketListeners,
             ILogger<WebSocketManager> logger,
             ILoggerFactory loggerFactory)
         {
+            _webSocketListeners = webSocketListeners;
             _logger = logger;
             _loggerFactory = loggerFactory;
         }
@@ -68,15 +69,6 @@ namespace Emby.Server.Implementations.HttpServer
             }
         }
 
-        /// <summary>
-        /// Adds the rest handlers.
-        /// </summary>
-        /// <param name="listeners">The web socket listeners.</param>
-        public void Init(IEnumerable<IWebSocketListener> listeners)
-        {
-            _webSocketListeners = listeners.ToArray();
-        }
-
         /// <summary>
         /// Processes the web socket message received.
         /// </summary>
@@ -90,7 +82,8 @@ namespace Emby.Server.Implementations.HttpServer
 
             IEnumerable<Task> GetTasks()
             {
-                foreach (var x in _webSocketListeners)
+                var listeners = _webSocketListeners.Value;
+                foreach (var x in listeners)
                 {
                     yield return x.ProcessMessageAsync(result);
                 }

+ 3 - 0
Emby.Server.Implementations/Localization/Core/hi.json

@@ -0,0 +1,3 @@
+{
+    "Albums": "आल्बुम्"
+}

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

@@ -112,5 +112,5 @@
     "Artists": "Artistë",
     "Application": "Aplikacioni",
     "AppDeviceValues": "Aplikacioni: {0}, Pajisja: {1}",
-    "Albums": "Albumet"
+    "Albums": "Albume"
 }

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

@@ -9,7 +9,7 @@
     "Channels": "Kanaler",
     "ChapterNameValue": "Kapitel {0}",
     "Collections": "Samlingar",
-    "DeviceOfflineWithName": "{0} har kopplat från",
+    "DeviceOfflineWithName": "{0} har kopplat ner",
     "DeviceOnlineWithName": "{0} är ansluten",
     "FailedLoginAttemptWithUserName": "Misslyckat inloggningsförsök från {0}",
     "Favorites": "Favoriter",

+ 2 - 2
Jellyfin.Api/Jellyfin.Api.csproj

@@ -14,9 +14,9 @@
 
   <ItemGroup>
     <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
-    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.8" />
+    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.9" />
     <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
-    <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.8" />
+    <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.9" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
     <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.6.3" />
   </ItemGroup>

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

@@ -41,8 +41,8 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.8" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.8" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.9" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.9" />
   </ItemGroup>
 
   <ItemGroup>

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

@@ -25,11 +25,11 @@
 
   <ItemGroup>
     <PackageReference Include="System.Linq.Async" Version="4.1.1" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.8">
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.9">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.8">
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.9">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>

+ 11 - 0
Jellyfin.Server/CoreAppHost.cs

@@ -4,6 +4,8 @@ using System.IO;
 using System.Reflection;
 using Emby.Drawing;
 using Emby.Server.Implementations;
+using Emby.Server.Implementations.Session;
+using Jellyfin.Api.WebSocketListeners;
 using Jellyfin.Drawing.Skia;
 using Jellyfin.Server.Implementations;
 using Jellyfin.Server.Implementations.Activity;
@@ -14,6 +16,7 @@ using MediaBrowser.Controller;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Activity;
 using MediaBrowser.Model.IO;
 using Microsoft.EntityFrameworkCore;
@@ -80,6 +83,14 @@ namespace Jellyfin.Server
             ServiceCollection.AddSingleton<IUserManager, UserManager>();
             ServiceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
 
+            ServiceCollection.AddScoped<IWebSocketListener, SessionWebSocketListener>();
+            ServiceCollection.AddScoped<IWebSocketListener, ActivityLogWebSocketListener>();
+            ServiceCollection.AddScoped<IWebSocketListener, ScheduledTasksWebSocketListener>();
+            ServiceCollection.AddScoped<IWebSocketListener, SessionInfoWebSocketListener>();
+
+            // TODO fix circular dependency on IWebSocketManager
+            ServiceCollection.AddScoped(serviceProvider => new Lazy<IEnumerable<IWebSocketListener>>(serviceProvider.GetRequiredService<IEnumerable<IWebSocketListener>>));
+
             base.RegisterServices();
         }
 

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

@@ -38,10 +38,10 @@
 
   <ItemGroup>
     <PackageReference Include="CommandLineParser" Version="2.8.0" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.8" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.8" />
-    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.8" />
-    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="3.1.8" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="3.1.9" />
     <PackageReference Include="prometheus-net" Version="3.6.0" />
     <PackageReference Include="prometheus-net.AspNetCore" Version="3.6.0" />
     <PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />

+ 1 - 1
Jellyfin.Server/Program.cs

@@ -378,7 +378,7 @@ namespace Jellyfin.Server
                 .ConfigureServices(services =>
                 {
                     // Merge the external ServiceCollection into ASP.NET DI
-                    services.TryAdd(serviceCollection);
+                    services.Add(serviceCollection);
                 })
                 .UseStartup<Startup>();
         }

+ 2 - 2
MediaBrowser.Common/MediaBrowser.Common.csproj

@@ -18,8 +18,8 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.8" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.9" />
     <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
     <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
   </ItemGroup>

+ 2 - 2
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -14,8 +14,8 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.8" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.9" />
     <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
   </ItemGroup>
 

+ 140 - 102
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -451,11 +451,13 @@ namespace MediaBrowser.Controller.MediaEncoding
             var arg = new StringBuilder();
             var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty;
             var outputVideoCodec = GetVideoEncoder(state, encodingOptions) ?? string.Empty;
+            var isSwDecoder = string.IsNullOrEmpty(videoDecoder);
+            var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1;
             var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
             var isVaapiEncoder = outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
             var isQsvDecoder = videoDecoder.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
             var isQsvEncoder = outputVideoCodec.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
-            var isNvencHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
+            var isNvdecHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
             var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
             var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
             var isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
@@ -517,11 +519,12 @@ namespace MediaBrowser.Controller.MediaEncoding
                 }
 
                 if (state.IsVideoRequest
-                    && string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase))
+                    && (string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && isNvdecHevcDecoder || isSwDecoder)
+                        || (string.Equals(encodingOptions.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && isD3d11vaDecoder || isSwDecoder))
                 {
                     var isColorDepth10 = IsColorDepth10(state);
 
-                    if (isNvencHevcDecoder && isColorDepth10
+                    if (isColorDepth10
                         && _mediaEncoder.SupportsHwaccel("opencl")
                         && encodingOptions.EnableTonemapping
                         && !string.IsNullOrEmpty(state.VideoStream.VideoRange)
@@ -880,6 +883,19 @@ namespace MediaBrowser.Controller.MediaEncoding
                         param += "-quality speed";
                         break;
                 }
+
+                var videoStream = state.VideoStream;
+                var isColorDepth10 = IsColorDepth10(state);
+
+                if (isColorDepth10
+                    && _mediaEncoder.SupportsHwaccel("opencl")
+                    && encodingOptions.EnableTonemapping
+                    && !string.IsNullOrEmpty(videoStream.VideoRange)
+                    && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
+                {
+                    // Enhance workload when tone mapping with AMF on some APUs
+                    param += " -preanalysis true";
+                }
             }
             else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // webm
             {
@@ -1023,19 +1039,19 @@ namespace MediaBrowser.Controller.MediaEncoding
                 && !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
                 && !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
                 && !string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
+                && !string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
                 && !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
             {
                 param = "-pix_fmt yuv420p " + param;
             }
 
-            if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
+            if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
+                || string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase))
             {
-                var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty;
                 var videoStream = state.VideoStream;
                 var isColorDepth10 = IsColorDepth10(state);
 
-                if (videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1
-                    && isColorDepth10
+                if (isColorDepth10
                     && _mediaEncoder.SupportsHwaccel("opencl")
                     && encodingOptions.EnableTonemapping
                     && !string.IsNullOrEmpty(videoStream.VideoRange)
@@ -1651,47 +1667,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             var outputSizeParam = ReadOnlySpan<char>.Empty;
             var request = state.BaseRequest;
 
-            outputSizeParam = GetOutputSizeParam(state, options, outputVideoCodec).TrimEnd('"');
-
-            // All possible beginning of video filters
-            // Don't break the order
-            string[] beginOfOutputSizeParam = new[]
-            {
-                // for tonemap_opencl
-                "hwupload,tonemap_opencl",
-
-                // hwupload=extra_hw_frames=64,vpp_qsv (for overlay_qsv on linux)
-                "hwupload=extra_hw_frames",
-
-                // vpp_qsv
-                "vpp",
-
-                // hwdownload,format=p010le (hardware decode + software encode for vaapi)
-                "hwdownload",
-
-                // format=nv12|vaapi,hwupload,scale_vaapi
-                "format",
-
-                // bwdif,scale=expr
-                "bwdif",
-
-                // yadif,scale=expr
-                "yadif",
-
-                // scale=expr
-                "scale"
-            };
-
-            var index = -1;
-            foreach (var param in beginOfOutputSizeParam)
-            {
-                index = outputSizeParam.IndexOf(param, StringComparison.OrdinalIgnoreCase);
-                if (index != -1)
-                {
-                    outputSizeParam = outputSizeParam.Slice(index);
-                    break;
-                }
-            }
+            outputSizeParam = GetOutputSizeParamInternal(state, options, outputVideoCodec);
 
             var videoSizeParam = string.Empty;
             var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options) ?? string.Empty;
@@ -2083,10 +2059,19 @@ namespace MediaBrowser.Controller.MediaEncoding
             return string.Format(CultureInfo.InvariantCulture, filter, widthParam, heightParam);
         }
 
+        public string GetOutputSizeParam(
+            EncodingJobInfo state,
+            EncodingOptions options,
+            string outputVideoCodec)
+        {
+            string filters = GetOutputSizeParamInternal(state, options, outputVideoCodec);
+            return string.IsNullOrEmpty(filters) ? string.Empty : " -vf \"" + filters + "\"";
+        }
+
         /// <summary>
         /// If we're going to put a fixed size on the command line, this will calculate it.
         /// </summary>
-        public string GetOutputSizeParam(
+        public string GetOutputSizeParamInternal(
             EncodingJobInfo state,
             EncodingOptions options,
             string outputVideoCodec)
@@ -2102,6 +2087,8 @@ namespace MediaBrowser.Controller.MediaEncoding
             var inputHeight = videoStream?.Height;
             var threeDFormat = state.MediaSource.Video3DFormat;
 
+            var isSwDecoder = string.IsNullOrEmpty(videoDecoder);
+            var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1;
             var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
             var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
             var isQsvH264Encoder = outputVideoCodec.IndexOf("h264_qsv", StringComparison.OrdinalIgnoreCase) != -1;
@@ -2117,47 +2104,77 @@ namespace MediaBrowser.Controller.MediaEncoding
             // If double rate deinterlacing is enabled and the input framerate is 30fps or below, otherwise the output framerate will be too high for many devices
             var doubleRateDeinterlace = options.DeinterlaceDoubleRate && (videoStream?.RealFrameRate ?? 60) <= 30;
 
-            // Currently only with the use of NVENC decoder can we get a decent performance.
-            // Currently only the HEVC/H265 format is supported.
-            // NVIDIA Pascal and Turing or higher are recommended.
-            if (isNvdecHevcDecoder && isColorDepth10
-                && _mediaEncoder.SupportsHwaccel("opencl")
-                && options.EnableTonemapping
-                && !string.IsNullOrEmpty(videoStream.VideoRange)
-                && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
-            {
-                var parameters = "tonemap_opencl=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:desat={1}:threshold={2}:peak={3}";
+            var isScalingInAdvance = false;
+            var isDeinterlaceH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
+            var isDeinterlaceHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
 
-                if (options.TonemappingParam != 0)
+            if ((string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && isNvdecHevcDecoder || isSwDecoder)
+                || (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && isD3d11vaDecoder || isSwDecoder))
+            {
+                // Currently only with the use of NVENC decoder can we get a decent performance.
+                // Currently only the HEVC/H265 format is supported with NVDEC decoder.
+                // NVIDIA Pascal and Turing or higher are recommended.
+                // AMD Polaris and Vega or higher are recommended.
+                if (isColorDepth10
+                    && _mediaEncoder.SupportsHwaccel("opencl")
+                    && options.EnableTonemapping
+                    && !string.IsNullOrEmpty(videoStream.VideoRange)
+                    && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
                 {
-                    parameters += ":param={4}";
-                }
+                    var parameters = "tonemap_opencl=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:desat={1}:threshold={2}:peak={3}";
 
-                if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase))
-                {
-                    parameters += ":range={5}";
-                }
+                    if (options.TonemappingParam != 0)
+                    {
+                        parameters += ":param={4}";
+                    }
 
-                // Upload the HDR10 or HLG data to the OpenCL device,
-                // use tonemap_opencl filter for tone mapping,
-                // and then download the SDR data to memory.
-                filters.Add("hwupload");
-                filters.Add(
-                    string.Format(
-                        CultureInfo.InvariantCulture,
-                        parameters,
-                        options.TonemappingAlgorithm,
-                        options.TonemappingDesat,
-                        options.TonemappingThreshold,
-                        options.TonemappingPeak,
-                        options.TonemappingParam,
-                        options.TonemappingRange));
-                filters.Add("hwdownload");
-
-                if (hasGraphicalSubs || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true)
-                    || string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
-                {
-                    filters.Add("format=nv12");
+                    if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase))
+                    {
+                        parameters += ":range={5}";
+                    }
+
+                    if (isSwDecoder || isD3d11vaDecoder)
+                    {
+                        isScalingInAdvance = true;
+                        // Add zscale filter before tone mapping filter for performance.
+                        var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight);
+                        if (width.HasValue && height.HasValue)
+                        {
+                            filters.Add(
+                                string.Format(
+                                    CultureInfo.InvariantCulture,
+                                    "zscale=s={0}x{1}",
+                                    width.Value,
+                                    height.Value));
+                        }
+
+                        // Convert to hardware pixel format p010 when using SW decoder.
+                        filters.Add("format=p010");
+                    }
+
+                    // Upload the HDR10 or HLG data to the OpenCL device,
+                    // use tonemap_opencl filter for tone mapping,
+                    // and then download the SDR data to memory.
+                    filters.Add("hwupload");
+                    filters.Add(
+                        string.Format(
+                            CultureInfo.InvariantCulture,
+                            parameters,
+                            options.TonemappingAlgorithm,
+                            options.TonemappingDesat,
+                            options.TonemappingThreshold,
+                            options.TonemappingPeak,
+                            options.TonemappingParam,
+                            options.TonemappingRange));
+                    filters.Add("hwdownload");
+
+                    if (isLibX264Encoder
+                        || hasGraphicalSubs
+                        || (isNvdecHevcDecoder && isDeinterlaceHevc)
+                        || (!isNvdecHevcDecoder && isDeinterlaceH264 || isDeinterlaceHevc))
+                    {
+                        filters.Add("format=nv12");
+                    }
                 }
             }
 
@@ -2202,7 +2219,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             }
 
             // Add hardware deinterlace filter before scaling filter
-            if (state.DeInterlace("h264", true) || state.DeInterlace("avc", true))
+            if (isDeinterlaceH264)
             {
                 if (isVaapiH264Encoder)
                 {
@@ -2215,10 +2232,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             }
 
             // Add software deinterlace filter before scaling filter
-            if ((state.DeInterlace("h264", true)
-                 || state.DeInterlace("avc", true)
-                 || state.DeInterlace("h265", true)
-                 || state.DeInterlace("hevc", true))
+            if ((isDeinterlaceH264 || isDeinterlaceHevc)
                 && !isVaapiH264Encoder
                 && !isQsvH264Encoder
                 && !isNvdecH264Decoder)
@@ -2242,7 +2256,21 @@ namespace MediaBrowser.Controller.MediaEncoding
             }
 
             // Add scaling filter: scale_*=format=nv12 or scale_*=w=*:h=*:format=nv12 or scale=expr
-            filters.AddRange(GetScalingFilters(state, inputWidth, inputHeight, threeDFormat, videoDecoder, outputVideoCodec, request.Width, request.Height, request.MaxWidth, request.MaxHeight));
+            if (!isScalingInAdvance)
+            {
+                filters.AddRange(
+                    GetScalingFilters(
+                        state,
+                        inputWidth,
+                        inputHeight,
+                        threeDFormat,
+                        videoDecoder,
+                        outputVideoCodec,
+                        request.Width,
+                        request.Height,
+                        request.MaxWidth,
+                        request.MaxHeight));
+            }
 
             // Add parameters to use VAAPI with burn-in text subtitles (GH issue #642)
             if (isVaapiH264Encoder)
@@ -2275,7 +2303,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             {
                 output += string.Format(
                     CultureInfo.InvariantCulture,
-                    " -vf \"{0}\"",
+                    "{0}",
                     string.Join(",", filters));
             }
 
@@ -3068,21 +3096,31 @@ namespace MediaBrowser.Controller.MediaEncoding
             var isWindows8orLater = Environment.OSVersion.Version.Major > 6 || (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor > 1);
             var isDxvaSupported = _mediaEncoder.SupportsHwaccel("dxva2") || _mediaEncoder.SupportsHwaccel("d3d11va");
 
-            if ((isDxvaSupported || IsVaapiSupported(state)) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase))
+            if (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase))
             {
-                if (isLinux)
+                // Currently there is no AMF decoder on Linux, only have h264 encoder.
+                if (isDxvaSupported && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase))
                 {
-                    return "-hwaccel vaapi";
-                }
+                    if (isWindows && isWindows8orLater)
+                    {
+                        return "-hwaccel d3d11va";
+                    }
 
-                if (isWindows && isWindows8orLater)
-                {
-                    return "-hwaccel d3d11va";
+                    if (isWindows && !isWindows8orLater)
+                    {
+                        return "-hwaccel dxva2";
+                    }
                 }
+            }
 
-                if (isWindows && !isWindows8orLater)
+            if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
+            {
+                if (IsVaapiSupported(state) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase))
                 {
-                    return "-hwaccel dxva2";
+                    if (isLinux)
+                    {
+                        return "-hwaccel vaapi";
+                    }
                 }
             }
 

+ 0 - 6
MediaBrowser.Controller/Net/IWebSocketManager.cs

@@ -16,12 +16,6 @@ namespace MediaBrowser.Controller.Net
         /// </summary>
         event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
 
-        /// <summary>
-        /// Inits this instance.
-        /// </summary>
-        /// <param name="listeners">The websocket listeners.</param>
-        void Init(IEnumerable<IWebSocketListener> listeners);
-
         /// <summary>
         /// The HTTP request handler.
         /// </summary>

+ 10 - 0
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs

@@ -666,6 +666,16 @@ namespace MediaBrowser.MediaEncoding.Probing
                 stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate);
                 stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate);
 
+                // Interlaced video streams in Matroska containers return the field rate instead of the frame rate
+                // as both the average and real frame rate, so we half the returned frame rates to get the correct values
+                //
+                // https://gitlab.com/mbunkus/mkvtoolnix/-/wikis/Wrong-frame-rate-displayed
+                if (stream.IsInterlaced && formatInfo.FormatName.Contains("matroska", StringComparison.OrdinalIgnoreCase))
+                {
+                    stream.AverageFrameRate /= 2;
+                    stream.RealFrameRate /= 2;
+                }
+
                 if (isAudio || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) ||
                     string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase))
                 {

+ 1 - 1
MediaBrowser.Model/MediaBrowser.Model.csproj

@@ -34,7 +34,7 @@
   <ItemGroup>
     <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
     <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
-    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.8" />
+    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.9" />
     <PackageReference Include="System.Globalization" Version="4.3.0" />
     <PackageReference Include="System.Text.Json" Version="5.0.0-preview.8.20407.11" />
   </ItemGroup>

+ 5 - 2
MediaBrowser.Model/System/PublicSystemInfo.cs

@@ -43,7 +43,10 @@ namespace MediaBrowser.Model.System
         /// <summary>
         /// Gets or sets a value indicating whether the startup wizard is completed.
         /// </summary>
-        /// <value>The startup completion status.</value>
-        public bool StartupWizardCompleted { get; set; }
+        /// <remarks>
+        /// Nullable for OpenAPI specification only to retain backwards compatibility in apiclients.
+        /// </remarks>
+        /// <value>The startup completion status.</value>]
+        public bool? StartupWizardCompleted { get; set; }
     }
 }

+ 3 - 3
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -16,9 +16,9 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
-    <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.8" />
-    <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.8" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.9" />
     <PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" />
     <PackageReference Include="PlaylistsNET" Version="1.1.2" />
     <PackageReference Include="TMDbLib" Version="1.7.3-alpha" />

+ 43 - 32
MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs

@@ -46,6 +46,7 @@ namespace MediaBrowser.Providers.Music
 
         private readonly string _musicBrainzBaseUrl;
 
+        private SemaphoreSlim _apiRequestLock = new SemaphoreSlim(1, 1);
         private Stopwatch _stopWatchMusicBrainz = new Stopwatch();
 
         public MusicBrainzAlbumProvider(
@@ -742,48 +743,58 @@ namespace MediaBrowser.Providers.Music
         /// </summary>
         internal async Task<HttpResponseMessage> GetMusicBrainzResponse(string url, CancellationToken cancellationToken)
         {
-            using var options = new HttpRequestMessage(HttpMethod.Get, _musicBrainzBaseUrl.TrimEnd('/') + url);
+            await _apiRequestLock.WaitAsync(cancellationToken).ConfigureAwait(false);
 
-            // MusicBrainz request a contact email address is supplied, as comment, in user agent field:
-            // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#User-Agent
-            options.Headers.UserAgent.ParseAdd(string.Format(
-                CultureInfo.InvariantCulture,
-                "{0} ( {1} )",
-                _appHost.ApplicationUserAgent,
-                _appHost.ApplicationUserAgentAddress));
-
-            HttpResponseMessage response;
-            var attempts = 0u;
-
-            do
+            try
             {
-                attempts++;
+                HttpResponseMessage response;
+                var attempts = 0u;
+                var requestUrl = _musicBrainzBaseUrl.TrimEnd('/') + url;
 
-                if (_stopWatchMusicBrainz.ElapsedMilliseconds < _musicBrainzQueryIntervalMs)
+                do
                 {
-                    // MusicBrainz is extremely adamant about limiting to one request per second
-                    var delayMs = _musicBrainzQueryIntervalMs - _stopWatchMusicBrainz.ElapsedMilliseconds;
-                    await Task.Delay((int)delayMs, cancellationToken).ConfigureAwait(false);
-                }
+                    attempts++;
 
-                // Write time since last request to debug log as evidence we're meeting rate limit
-                // requirement, before resetting stopwatch back to zero.
-                _logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds);
-                _stopWatchMusicBrainz.Restart();
+                    if (_stopWatchMusicBrainz.ElapsedMilliseconds < _musicBrainzQueryIntervalMs)
+                    {
+                        // MusicBrainz is extremely adamant about limiting to one request per second.
+                        var delayMs = _musicBrainzQueryIntervalMs - _stopWatchMusicBrainz.ElapsedMilliseconds;
+                        await Task.Delay((int)delayMs, cancellationToken).ConfigureAwait(false);
+                    }
 
-                response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options).ConfigureAwait(false);
+                    // Write time since last request to debug log as evidence we're meeting rate limit
+                    // requirement, before resetting stopwatch back to zero.
+                    _logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds);
+                    _stopWatchMusicBrainz.Restart();
 
-                // We retry a finite number of times, and only whilst MB is indicating 503 (throttling)
-            }
-            while (attempts < MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable);
+                    using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
+
+                    // MusicBrainz request a contact email address is supplied, as comment, in user agent field:
+                    // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#User-Agent .
+                    request.Headers.UserAgent.ParseAdd(string.Format(
+                        CultureInfo.InvariantCulture,
+                        "{0} ( {1} )",
+                        _appHost.ApplicationUserAgent,
+                        _appHost.ApplicationUserAgentAddress));
 
-            // Log error if unable to query MB database due to throttling
-            if (attempts == MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable)
+                    response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(request).ConfigureAwait(false);
+
+                    // We retry a finite number of times, and only whilst MB is indicating 503 (throttling).
+                }
+                while (attempts < MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable);
+
+                // Log error if unable to query MB database due to throttling.
+                if (attempts == MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable)
+                {
+                    _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, requestUrl);
+                }
+
+                return response;
+            }
+            finally
             {
-                _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, options.RequestUri);
+                _apiRequestLock.Release();
             }
-
-            return response;
         }
 
         /// <inheritdoc />

+ 2 - 2
debian/postrm

@@ -25,7 +25,7 @@ case "$1" in
   purge)
     echo PURGE | debconf-communicate $NAME > /dev/null 2>&1 || true
 
-    if [[ -x "/etc/init.d/jellyfin" ]] || [[ -e "/etc/init/jellyfin.connf" ]]; then
+    if [[ -x "/etc/init.d/jellyfin" ]] || [[ -e "/etc/init/jellyfin.conf" ]]; then
       update-rc.d jellyfin remove >/dev/null 2>&1 || true
     fi
 
@@ -54,7 +54,7 @@ case "$1" in
       rm -rf $PROGRAMDATA
     fi
     # Remove binary symlink
-    [[ -f /usr/bin/jellyfin ]] && rm /usr/bin/jellyfin
+    rm -f /usr/bin/jellyfin
     # Remove sudoers config
     [[ -f /etc/sudoers.d/jellyfin-sudoers ]] && rm /etc/sudoers.d/jellyfin-sudoers
     # Remove anything at the default locations; catches situations where the user moved the defaults

+ 1 - 1
deployment/Dockerfile.debian.amd64

@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.debian.arm64

@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.debian.armhf

@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.linux.amd64

@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.macos

@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.portable

@@ -15,7 +15,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.ubuntu.amd64

@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.ubuntu.arm64

@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.ubuntu.armhf

@@ -16,7 +16,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

+ 1 - 1
deployment/Dockerfile.windows.amd64

@@ -15,7 +15,7 @@ RUN apt-get update \
 
 # Install dotnet repository
 # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/f01e3d97-c1c3-4635-bc77-0c893be36820/6ec6acabc22468c6cc68b61625b14a7d/dotnet-sdk-3.1.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/fdd9ecec-56b4-40f4-b762-d7efe24fc3cd/ffef51844c92afa6714528e10609a30f/dotnet-sdk-3.1.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
  && mkdir -p dotnet-sdk \
  && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
  && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

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

@@ -16,8 +16,8 @@
     <PackageReference Include="AutoFixture" Version="4.13.0" />
     <PackageReference Include="AutoFixture.AutoMoq" Version="4.13.0" />
     <PackageReference Include="AutoFixture.Xunit2" Version="4.13.0" />
-    <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.8" />
-    <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.8" />
+    <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.9" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />

+ 14 - 7
tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs

@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using Emby.Naming.AudioBook;
 using Emby.Naming.Common;
 using Xunit;
@@ -42,16 +43,22 @@ namespace Jellyfin.Naming.Tests.AudioBook
 
         [Theory]
         [MemberData(nameof(GetResolveFileTestData))]
-        public void ResolveFile_ValidFileName_Success(AudioBookFileInfo expectedResult)
+        public void Resolve_ValidFileName_Success(AudioBookFileInfo expectedResult)
         {
             var result = new AudioBookResolver(_namingOptions).Resolve(expectedResult.Path);
 
             Assert.NotNull(result);
-            Assert.Equal(result.Path, expectedResult.Path);
-            Assert.Equal(result.Container, expectedResult.Container);
-            Assert.Equal(result.ChapterNumber, expectedResult.ChapterNumber);
-            Assert.Equal(result.PartNumber, expectedResult.PartNumber);
-            Assert.Equal(result.IsDirectory, expectedResult.IsDirectory);
+            Assert.Equal(result!.Path, expectedResult.Path);
+            Assert.Equal(result!.Container, expectedResult.Container);
+            Assert.Equal(result!.ChapterNumber, expectedResult.ChapterNumber);
+            Assert.Equal(result!.PartNumber, expectedResult.PartNumber);
+            Assert.Equal(result!.IsDirectory, expectedResult.IsDirectory);
+        }
+
+        [Fact]
+        public void Resolve_EmptyFileName_ArgumentException()
+        {
+            Assert.Throws<ArgumentException>(() => new AudioBookResolver(_namingOptions).Resolve(string.Empty));
         }
     }
 }