Browse Source

Merge branch 'master' into displaypreferences-efcore

Patrick Barron 4 years ago
parent
commit
3d69cea1c9
79 changed files with 1243 additions and 855 deletions
  1. 22 0
      .ci/azure-pipelines-package.yml
  2. 5 2
      Emby.Dlna/Api/DlnaServerService.cs
  3. 9 5
      Emby.Dlna/Eventing/EventManager.cs
  4. 2 2
      Emby.Dlna/PlayTo/Device.cs
  5. 75 107
      Emby.Dlna/Server/DescriptionXmlBuilder.cs
  6. 25 9
      Emby.Dlna/Service/ServiceXmlBuilder.cs
  7. 30 30
      Emby.Naming/Common/NamingOptions.cs
  8. 2 3
      Emby.Server.Implementations/ApplicationHost.cs
  9. 9 5
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  10. 4 4
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  11. 11 2
      Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
  12. 10 0
      Emby.Server.Implementations/IO/ManagedFileSystem.cs
  13. 1 1
      Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
  14. 12 12
      Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
  15. 119 142
      Emby.Server.Implementations/Library/LibraryManager.cs
  16. 12 12
      Emby.Server.Implementations/Library/LiveStreamHelper.cs
  17. 9 8
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  18. 1 1
      Emby.Server.Implementations/Library/MediaStreamSelector.cs
  19. 6 12
      Emby.Server.Implementations/Library/SearchEngine.cs
  20. 3 6
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  21. 1 1
      Emby.Server.Implementations/Localization/Core/de.json
  22. 32 32
      Emby.Server.Implementations/Localization/Core/ms.json
  23. 3 1
      Emby.Server.Implementations/Localization/Core/th.json
  24. 12 1
      Emby.Server.Implementations/Networking/NetworkManager.cs
  25. 0 1
      Emby.Server.Implementations/Services/ServiceController.cs
  26. 3 1
      Emby.Server.Implementations/Services/ServiceHandler.cs
  27. 4 2
      Emby.Server.Implementations/Services/ServicePath.cs
  28. 13 16
      Emby.Server.Implementations/TV/TVSeriesManager.cs
  29. 5 0
      Emby.Server.Implementations/Updates/InstallationManager.cs
  30. 3 5
      Jellyfin.Api/Controllers/StartupController.cs
  31. 1 1
      Jellyfin.Api/Jellyfin.Api.csproj
  32. 2 2
      Jellyfin.Data/Jellyfin.Data.csproj
  33. 2 2
      Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
  34. 13 0
      Jellyfin.Server.Implementations/JellyfinDb.cs
  35. 9 7
      Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
  36. 66 43
      Jellyfin.Server.Implementations/Users/UserManager.cs
  37. 2 2
      Jellyfin.Server/CoreAppHost.cs
  38. 2 2
      Jellyfin.Server/Jellyfin.Server.csproj
  39. 5 0
      Jellyfin.Server/Migrations/IMigrationRoutine.cs
  40. 2 2
      Jellyfin.Server/Migrations/MigrationRunner.cs
  41. 3 0
      Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs
  42. 3 0
      Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
  43. 3 0
      Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
  44. 3 0
      Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
  45. 3 0
      Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
  46. 49 0
      Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs
  47. 3 0
      Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
  48. 15 0
      Jellyfin.Server/Program.cs
  49. 5 0
      MediaBrowser.Api/Images/ImageService.cs
  50. 4 1
      MediaBrowser.Api/System/ActivityLogService.cs
  51. 1 1
      MediaBrowser.Api/UserService.cs
  52. 33 0
      MediaBrowser.Common/Extensions/HttpContextExtensions.cs
  53. 2 2
      MediaBrowser.Common/MediaBrowser.Common.csproj
  54. 1 1
      MediaBrowser.Controller/Library/ILibraryManager.cs
  55. 2 5
      MediaBrowser.Controller/Library/IUserManager.cs
  56. 2 2
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  57. 249 146
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  58. 2 2
      MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
  59. 1 1
      MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
  60. 1 1
      MediaBrowser.Model/MediaBrowser.Model.csproj
  61. 10 3
      MediaBrowser.Model/Notifications/NotificationOption.cs
  62. 143 121
      MediaBrowser.Providers/Manager/ProviderManager.cs
  63. 2 2
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  64. 22 20
      MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html
  65. 38 24
      MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html
  66. 19 16
      MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html
  67. 40 0
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs
  68. 2 2
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs
  69. 3 2
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs
  70. 12 3
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs
  71. 2 1
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs
  72. 1 1
      MediaBrowser.Providers/TV/SeriesMetadataService.cs
  73. 6 0
      debian/changelog
  74. 2 2
      debian/conf/jellyfin
  75. 2 3
      debian/control
  76. 4 6
      deployment/build.windows.amd64
  77. 1 1
      tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
  78. 1 1
      tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj
  79. 1 1
      windows/build-jellyfin.ps1

+ 22 - 0
.ci/azure-pipelines-package.yml

@@ -139,3 +139,25 @@ jobs:
         sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)
         rm $0
         exit
+
+- job: PublishNuget
+  displayName: 'Publish NuGet packages'
+  dependsOn:
+  - BuildPackage
+  condition: and(succeeded('BuildPackage'), startsWith(variables['Build.SourceBranch'], 'refs/tags'))
+  
+  pool:
+    vmImage: 'ubuntu-latest'
+
+  steps:
+  - task: NuGetCommand@2
+    inputs:
+      command: 'pack'
+      packagesToPack: Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj
+      packDestination: '$(Build.ArtifactStagingDirectory)'
+
+  - task: NuGetCommand@2
+    inputs:
+      command: 'push'
+      packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
+      includeNugetOrg: 'true'

+ 5 - 2
Emby.Dlna/Api/DlnaServerService.cs

@@ -11,6 +11,7 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
 
 namespace Emby.Dlna.Api
 {
@@ -108,7 +109,7 @@ namespace Emby.Dlna.Api
         public string Filename { get; set; }
     }
 
-    public class DlnaServerService : IService, IRequiresRequest
+    public class DlnaServerService : IService
     {
         private const string XMLContentType = "text/xml; charset=UTF-8";
 
@@ -127,11 +128,13 @@ namespace Emby.Dlna.Api
         public DlnaServerService(
             IDlnaManager dlnaManager,
             IHttpResultFactory httpResultFactory,
-            IServerConfigurationManager configurationManager)
+            IServerConfigurationManager configurationManager,
+            IHttpContextAccessor httpContextAccessor)
         {
             _dlnaManager = dlnaManager;
             _resultFactory = httpResultFactory;
             _configurationManager = configurationManager;
+            Request = httpContextAccessor?.HttpContext.GetServiceStackRequest() ?? throw new ArgumentNullException(nameof(httpContextAccessor));
         }
 
         private string GetHeader(string name)

+ 9 - 5
Emby.Dlna/Eventing/EventManager.cs

@@ -152,11 +152,15 @@ namespace Emby.Dlna.Eventing
             builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
             foreach (var key in stateVariables.Keys)
             {
-                builder.Append("<e:property>");
-                builder.Append("<" + key + ">");
-                builder.Append(stateVariables[key]);
-                builder.Append("</" + key + ">");
-                builder.Append("</e:property>");
+                builder.Append("<e:property>")
+                    .Append('<')
+                    .Append(key)
+                    .Append('>')
+                    .Append(stateVariables[key])
+                    .Append("</")
+                    .Append(key)
+                    .Append('>')
+                    .Append("</e:property>");
             }
 
             builder.Append("</e:propertyset>");

+ 2 - 2
Emby.Dlna/PlayTo/Device.cs

@@ -4,12 +4,12 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using System.Security;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Xml;
 using System.Xml.Linq;
 using Emby.Dlna.Common;
-using Emby.Dlna.Server;
 using Emby.Dlna.Ssdp;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
@@ -334,7 +334,7 @@ namespace Emby.Dlna.PlayTo
                 return string.Empty;
             }
 
-            return DescriptionXmlBuilder.Escape(value);
+            return SecurityElement.Escape(value);
         }
 
         private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)

+ 75 - 107
Emby.Dlna/Server/DescriptionXmlBuilder.cs

@@ -4,6 +4,7 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using System.Security;
 using System.Text;
 using Emby.Dlna.Common;
 using MediaBrowser.Model.Dlna;
@@ -64,10 +65,10 @@ namespace Emby.Dlna.Server
 
             foreach (var att in attributes)
             {
-                builder.AppendFormat(" {0}=\"{1}\"", att.Name, att.Value);
+                builder.AppendFormat(CultureInfo.InvariantCulture, " {0}=\"{1}\"", att.Name, att.Value);
             }
 
-            builder.Append(">");
+            builder.Append('>');
 
             builder.Append("<specVersion>");
             builder.Append("<major>1</major>");
@@ -76,7 +77,9 @@ namespace Emby.Dlna.Server
 
             if (!EnableAbsoluteUrls)
             {
-                builder.Append("<URLBase>" + Escape(_serverAddress) + "</URLBase>");
+                builder.Append("<URLBase>")
+                    .Append(SecurityElement.Escape(_serverAddress))
+                    .Append("</URLBase>");
             }
 
             AppendDeviceInfo(builder);
@@ -93,91 +96,14 @@ namespace Emby.Dlna.Server
 
             AppendIconList(builder);
 
-            builder.Append("<presentationURL>" + Escape(_serverAddress) + "/web/index.html</presentationURL>");
+            builder.Append("<presentationURL>")
+                .Append(SecurityElement.Escape(_serverAddress))
+                .Append("/web/index.html</presentationURL>");
 
             AppendServiceList(builder);
             builder.Append("</device>");
         }
 
-        private static readonly char[] s_escapeChars = new char[]
-        {
-            '<',
-            '>',
-            '"',
-            '\'',
-            '&'
-        };
-
-        private static readonly string[] s_escapeStringPairs = new[]
-        {
-            "<",
-            "&lt;",
-            ">",
-            "&gt;",
-            "\"",
-            "&quot;",
-            "'",
-            "&apos;",
-            "&",
-            "&amp;"
-        };
-
-        private static string GetEscapeSequence(char c)
-        {
-            int num = s_escapeStringPairs.Length;
-            for (int i = 0; i < num; i += 2)
-            {
-                string text = s_escapeStringPairs[i];
-                string result = s_escapeStringPairs[i + 1];
-                if (text[0] == c)
-                {
-                    return result;
-                }
-            }
-
-            return c.ToString(CultureInfo.InvariantCulture);
-        }
-
-        /// <summary>Replaces invalid XML characters in a string with their valid XML equivalent.</summary>
-        /// <returns>The input string with invalid characters replaced.</returns>
-        /// <param name="str">The string within which to escape invalid characters. </param>
-        public static string Escape(string str)
-        {
-            if (str == null)
-            {
-                return null;
-            }
-
-            StringBuilder stringBuilder = null;
-            int length = str.Length;
-            int num = 0;
-            while (true)
-            {
-                int num2 = str.IndexOfAny(s_escapeChars, num);
-                if (num2 == -1)
-                {
-                    break;
-                }
-
-                if (stringBuilder == null)
-                {
-                    stringBuilder = new StringBuilder();
-                }
-
-                stringBuilder.Append(str, num, num2 - num);
-                stringBuilder.Append(GetEscapeSequence(str[num2]));
-                num = num2 + 1;
-            }
-
-            if (stringBuilder == null)
-            {
-                return str;
-            }
-
-            stringBuilder.Append(str, num, length - num);
-            return stringBuilder.ToString();
-        }
-
         private void AppendDeviceProperties(StringBuilder builder)
         {
             builder.Append("<dlna:X_DLNACAP/>");
@@ -187,32 +113,54 @@ namespace Emby.Dlna.Server
 
             builder.Append("<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>");
 
-            builder.Append("<friendlyName>" + Escape(GetFriendlyName()) + "</friendlyName>");
-            builder.Append("<manufacturer>" + Escape(_profile.Manufacturer ?? string.Empty) + "</manufacturer>");
-            builder.Append("<manufacturerURL>" + Escape(_profile.ManufacturerUrl ?? string.Empty) + "</manufacturerURL>");
-
-            builder.Append("<modelDescription>" + Escape(_profile.ModelDescription ?? string.Empty) + "</modelDescription>");
-            builder.Append("<modelName>" + Escape(_profile.ModelName ?? string.Empty) + "</modelName>");
-
-            builder.Append("<modelNumber>" + Escape(_profile.ModelNumber ?? string.Empty) + "</modelNumber>");
-            builder.Append("<modelURL>" + Escape(_profile.ModelUrl ?? string.Empty) + "</modelURL>");
+            builder.Append("<friendlyName>")
+                .Append(SecurityElement.Escape(GetFriendlyName()))
+                .Append("</friendlyName>");
+            builder.Append("<manufacturer>")
+                .Append(SecurityElement.Escape(_profile.Manufacturer ?? string.Empty))
+                .Append("</manufacturer>");
+            builder.Append("<manufacturerURL>")
+                .Append(SecurityElement.Escape(_profile.ManufacturerUrl ?? string.Empty))
+                .Append("</manufacturerURL>");
+
+            builder.Append("<modelDescription>")
+                .Append(SecurityElement.Escape(_profile.ModelDescription ?? string.Empty))
+                .Append("</modelDescription>");
+            builder.Append("<modelName>")
+                .Append(SecurityElement.Escape(_profile.ModelName ?? string.Empty))
+                .Append("</modelName>");
+
+            builder.Append("<modelNumber>")
+                .Append(SecurityElement.Escape(_profile.ModelNumber ?? string.Empty))
+                .Append("</modelNumber>");
+            builder.Append("<modelURL>")
+                .Append(SecurityElement.Escape(_profile.ModelUrl ?? string.Empty))
+                .Append("</modelURL>");
 
             if (string.IsNullOrEmpty(_profile.SerialNumber))
             {
-                builder.Append("<serialNumber>" + Escape(_serverId) + "</serialNumber>");
+                builder.Append("<serialNumber>")
+                    .Append(SecurityElement.Escape(_serverId))
+                    .Append("</serialNumber>");
             }
             else
             {
-                builder.Append("<serialNumber>" + Escape(_profile.SerialNumber) + "</serialNumber>");
+                builder.Append("<serialNumber>")
+                    .Append(SecurityElement.Escape(_profile.SerialNumber))
+                    .Append("</serialNumber>");
             }
 
             builder.Append("<UPC/>");
 
-            builder.Append("<UDN>uuid:" + Escape(_serverUdn) + "</UDN>");
+            builder.Append("<UDN>uuid:")
+                .Append(SecurityElement.Escape(_serverUdn))
+                .Append("</UDN>");
 
             if (!string.IsNullOrEmpty(_profile.SonyAggregationFlags))
             {
-                builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">" + Escape(_profile.SonyAggregationFlags) + "</av:aggregationFlags>");
+                builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">")
+                    .Append(SecurityElement.Escape(_profile.SonyAggregationFlags))
+                    .Append("</av:aggregationFlags>");
             }
         }
 
@@ -250,11 +198,21 @@ namespace Emby.Dlna.Server
             {
                 builder.Append("<icon>");
 
-                builder.Append("<mimetype>" + Escape(icon.MimeType ?? string.Empty) + "</mimetype>");
-                builder.Append("<width>" + Escape(icon.Width.ToString(_usCulture)) + "</width>");
-                builder.Append("<height>" + Escape(icon.Height.ToString(_usCulture)) + "</height>");
-                builder.Append("<depth>" + Escape(icon.Depth ?? string.Empty) + "</depth>");
-                builder.Append("<url>" + BuildUrl(icon.Url) + "</url>");
+                builder.Append("<mimetype>")
+                    .Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
+                    .Append("</mimetype>");
+                builder.Append("<width>")
+                    .Append(SecurityElement.Escape(icon.Width.ToString(_usCulture)))
+                    .Append("</width>");
+                builder.Append("<height>")
+                    .Append(SecurityElement.Escape(icon.Height.ToString(_usCulture)))
+                    .Append("</height>");
+                builder.Append("<depth>")
+                    .Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
+                    .Append("</depth>");
+                builder.Append("<url>")
+                    .Append(BuildUrl(icon.Url))
+                    .Append("</url>");
 
                 builder.Append("</icon>");
             }
@@ -270,11 +228,21 @@ namespace Emby.Dlna.Server
             {
                 builder.Append("<service>");
 
-                builder.Append("<serviceType>" + Escape(service.ServiceType ?? string.Empty) + "</serviceType>");
-                builder.Append("<serviceId>" + Escape(service.ServiceId ?? string.Empty) + "</serviceId>");
-                builder.Append("<SCPDURL>" + BuildUrl(service.ScpdUrl) + "</SCPDURL>");
-                builder.Append("<controlURL>" + BuildUrl(service.ControlUrl) + "</controlURL>");
-                builder.Append("<eventSubURL>" + BuildUrl(service.EventSubUrl) + "</eventSubURL>");
+                builder.Append("<serviceType>")
+                    .Append(SecurityElement.Escape(service.ServiceType ?? string.Empty))
+                    .Append("</serviceType>");
+                builder.Append("<serviceId>")
+                    .Append(SecurityElement.Escape(service.ServiceId ?? string.Empty))
+                    .Append("</serviceId>");
+                builder.Append("<SCPDURL>")
+                    .Append(BuildUrl(service.ScpdUrl))
+                    .Append("</SCPDURL>");
+                builder.Append("<controlURL>")
+                    .Append(BuildUrl(service.ControlUrl))
+                    .Append("</controlURL>");
+                builder.Append("<eventSubURL>")
+                    .Append(BuildUrl(service.EventSubUrl))
+                    .Append("</eventSubURL>");
 
                 builder.Append("</service>");
             }
@@ -298,7 +266,7 @@ namespace Emby.Dlna.Server
                 url = _serverAddress.TrimEnd('/') + url;
             }
 
-            return Escape(url);
+            return SecurityElement.Escape(url);
         }
 
         private IEnumerable<DeviceIcon> GetIcons()

+ 25 - 9
Emby.Dlna/Service/ServiceXmlBuilder.cs

@@ -1,9 +1,9 @@
 #pragma warning disable CS1591
 
 using System.Collections.Generic;
+using System.Security;
 using System.Text;
 using Emby.Dlna.Common;
-using Emby.Dlna.Server;
 
 namespace Emby.Dlna.Service
 {
@@ -37,7 +37,9 @@ namespace Emby.Dlna.Service
             {
                 builder.Append("<action>");
 
-                builder.Append("<name>" + DescriptionXmlBuilder.Escape(item.Name ?? string.Empty) + "</name>");
+                builder.Append("<name>")
+                    .Append(SecurityElement.Escape(item.Name ?? string.Empty))
+                    .Append("</name>");
 
                 builder.Append("<argumentList>");
 
@@ -45,9 +47,15 @@ namespace Emby.Dlna.Service
                 {
                     builder.Append("<argument>");
 
-                    builder.Append("<name>" + DescriptionXmlBuilder.Escape(argument.Name ?? string.Empty) + "</name>");
-                    builder.Append("<direction>" + DescriptionXmlBuilder.Escape(argument.Direction ?? string.Empty) + "</direction>");
-                    builder.Append("<relatedStateVariable>" + DescriptionXmlBuilder.Escape(argument.RelatedStateVariable ?? string.Empty) + "</relatedStateVariable>");
+                    builder.Append("<name>")
+                        .Append(SecurityElement.Escape(argument.Name ?? string.Empty))
+                        .Append("</name>");
+                    builder.Append("<direction>")
+                        .Append(SecurityElement.Escape(argument.Direction ?? string.Empty))
+                        .Append("</direction>");
+                    builder.Append("<relatedStateVariable>")
+                        .Append(SecurityElement.Escape(argument.RelatedStateVariable ?? string.Empty))
+                        .Append("</relatedStateVariable>");
 
                     builder.Append("</argument>");
                 }
@@ -68,17 +76,25 @@ namespace Emby.Dlna.Service
             {
                 var sendEvents = item.SendsEvents ? "yes" : "no";
 
-                builder.Append("<stateVariable sendEvents=\"" + sendEvents + "\">");
+                builder.Append("<stateVariable sendEvents=\"")
+                    .Append(sendEvents)
+                    .Append("\">");
 
-                builder.Append("<name>" + DescriptionXmlBuilder.Escape(item.Name ?? string.Empty) + "</name>");
-                builder.Append("<dataType>" + DescriptionXmlBuilder.Escape(item.DataType ?? string.Empty) + "</dataType>");
+                builder.Append("<name>")
+                    .Append(SecurityElement.Escape(item.Name ?? string.Empty))
+                    .Append("</name>");
+                builder.Append("<dataType>")
+                    .Append(SecurityElement.Escape(item.DataType ?? string.Empty))
+                    .Append("</dataType>");
 
                 if (item.AllowedValues.Length > 0)
                 {
                     builder.Append("<allowedValueList>");
                     foreach (var allowedValue in item.AllowedValues)
                     {
-                        builder.Append("<allowedValue>" + DescriptionXmlBuilder.Escape(allowedValue) + "</allowedValue>");
+                        builder.Append("<allowedValue>")
+                            .Append(SecurityElement.Escape(allowedValue))
+                            .Append("</allowedValue>");
                     }
 
                     builder.Append("</allowedValueList>");

+ 30 - 30
Emby.Naming/Common/NamingOptions.cs

@@ -136,8 +136,8 @@ namespace Emby.Naming.Common
 
             CleanDateTimes = new[]
             {
-                @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*",
-                @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*"
+                @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*",
+                @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
             };
 
             CleanStrings = new[]
@@ -277,7 +277,7 @@ namespace Emby.Naming.Common
                 // This isn't a Kodi naming rule, but the expression below causes false positives,
                 // so we make sure this one gets tested first.
                 // "Foo Bar 889"
-                new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/x]*$")
+                new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/x]*$")
                 {
                     IsNamed = true
                 },
@@ -300,32 +300,32 @@ namespace Emby.Naming.Common
                 // *** End Kodi Standard Naming
 
                 // [bar] Foo - 1 [baz]
-                new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>\d+).*$")
+                new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>[0-9]+).*$")
                 {
                     IsNamed = true
                 },
-                new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>\d+)[xX](?<epnumber>\d+)[^\\\/]*$")
+                new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
                 {
                     IsNamed = true
                 },
 
-                new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>\d+)[x,X]?[eE](?<epnumber>\d+)[^\\\/]*$")
+                new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>[0-9]+)[x,X]?[eE](?<epnumber>[0-9]+)[^\\\/]*$")
                 {
                     IsNamed = true
                 },
 
-                new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d+))[^\\\/]*$")
+                new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]+))[^\\\/]*$")
                 {
                     IsNamed = true
                 },
 
-                new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d+)[^\\\/]*$")
+                new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]+)[^\\\/]*$")
                 {
                     IsNamed = true
                 },
 
                 // "01.avi"
-                new EpisodeExpression(@".*[\\\/](?<epnumber>\d+)(-(?<endingepnumber>\d+))*\.\w+$")
+                new EpisodeExpression(@".*[\\\/](?<epnumber>[0-9]+)(-(?<endingepnumber>[0-9]+))*\.\w+$")
                 {
                     IsOptimistic = true,
                     IsNamed = true
@@ -335,34 +335,34 @@ namespace Emby.Naming.Common
                 new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
 
                 // "01 - blah.avi", "01-blah.avi"
-                new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\s?-\s?[^\\\/]*$")
+                new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\s?-\s?[^\\\/]*$")
                 {
                     IsOptimistic = true,
                     IsNamed = true
                 },
 
                 // "01.blah.avi"
-                new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\.[^\\\/]+$")
+                new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\.[^\\\/]+$")
                 {
                     IsOptimistic = true,
                     IsNamed = true
                 },
 
                 // "blah - 01.avi", "blah 2 - 01.avi", "blah - 01 blah.avi", "blah 2 - 01 blah", "blah - 01 - blah.avi", "blah 2 - 01 - blah"
-                new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$")
+                new EpisodeExpression(@".*[\\\/][^\\\/]* - (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
                 {
                     IsOptimistic = true,
                     IsNamed = true
                 },
 
                 // "01 episode title.avi"
-                new EpisodeExpression(@"[Ss]eason[\._ ](?<seasonnumber>[0-9]+)[\\\/](?<epnumber>\d{1,3})([^\\\/]*)$")
+                new EpisodeExpression(@"[Ss]eason[\._ ](?<seasonnumber>[0-9]+)[\\\/](?<epnumber>[0-9]{1,3})([^\\\/]*)$")
                 {
                     IsOptimistic = true,
                     IsNamed = true
                 },
                 // "Episode 16", "Episode 16 - Title"
-                new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$")
+                new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
                 {
                     IsOptimistic = true,
                     IsNamed = true
@@ -625,17 +625,17 @@ namespace Emby.Naming.Common
             AudioBookPartsExpressions = new[]
             {
                 // Detect specified chapters, like CH 01
-                @"ch(?:apter)?[\s_-]?(?<chapter>\d+)",
+                @"ch(?:apter)?[\s_-]?(?<chapter>[0-9]+)",
                 // Detect specified parts, like Part 02
-                @"p(?:ar)?t[\s_-]?(?<part>\d+)",
+                @"p(?:ar)?t[\s_-]?(?<part>[0-9]+)",
                 // Chapter is often beginning of filename
-                @"^(?<chapter>\d+)",
+                "^(?<chapter>[0-9]+)",
                 // Part if often ending of filename
-                @"(?<part>\d+)$",
+                "(?<part>[0-9]+)$",
                 // Sometimes named as 0001_005 (chapter_part)
-                @"(?<chapter>\d+)_(?<part>\d+)",
+                "(?<chapter>[0-9]+)_(?<part>[0-9]+)",
                 // Some audiobooks are ripped from cd's, and will be named by disk number.
-                @"dis(?:c|k)[\s_-]?(?<chapter>\d+)"
+                @"dis(?:c|k)[\s_-]?(?<chapter>[0-9]+)"
             };
 
             var extensions = VideoFileExtensions.ToList();
@@ -675,16 +675,16 @@ namespace Emby.Naming.Common
 
             MultipleEpisodeExpressions = new string[]
             {
-                @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[eExX](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})(-[xE]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )\d{1,4}[xX][eE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
-                @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$"
+                @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})(-[xE]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )[0-9]{1,4}[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )[0-9]{1,4}[xX][eE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)(?<seriesname>((?![sS]?[0-9]{1,4}[xX][0-9]{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3}))(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})((-| - )?[xXeE](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
+                @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>[0-9]{1,4})[xX\.]?[eE](?<epnumber>[0-9]{1,3})(-[xX]?[eE]?(?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$"
             }.Select(i => new EpisodeExpression(i)
             {
                 IsNamed = true

+ 2 - 3
Emby.Server.Implementations/ApplicationHost.cs

@@ -192,7 +192,7 @@ namespace Emby.Server.Implementations
         /// Gets or sets the application paths.
         /// </summary>
         /// <value>The application paths.</value>
-        protected ServerApplicationPaths ApplicationPaths { get; set; }
+        protected IServerApplicationPaths ApplicationPaths { get; set; }
 
         /// <summary>
         /// Gets or sets all concrete types.
@@ -236,7 +236,7 @@ namespace Emby.Server.Implementations
         /// Initializes a new instance of the <see cref="ApplicationHost" /> class.
         /// </summary>
         public ApplicationHost(
-            ServerApplicationPaths applicationPaths,
+            IServerApplicationPaths applicationPaths,
             ILoggerFactory loggerFactory,
             IStartupOptions options,
             IFileSystem fileSystem,
@@ -792,7 +792,6 @@ namespace Emby.Server.Implementations
             Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
 
             Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
-            Resolve<IUserManager>().AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
 
             Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
         }

+ 9 - 5
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -1111,7 +1111,8 @@ namespace Emby.Server.Implementations.Data
                     continue;
                 }
 
-                str.Append(ToValueString(i) + "|");
+                str.Append(ToValueString(i))
+                    .Append('|');
             }
 
             str.Length -= 1; // Remove last |
@@ -2472,7 +2473,7 @@ namespace Emby.Server.Implementations.Data
                 var item = query.SimilarTo;
 
                 var builder = new StringBuilder();
-                builder.Append("(");
+                builder.Append('(');
 
                 if (string.IsNullOrEmpty(item.OfficialRating))
                 {
@@ -2510,7 +2511,7 @@ namespace Emby.Server.Implementations.Data
             if (!string.IsNullOrEmpty(query.SearchTerm))
             {
                 var builder = new StringBuilder();
-                builder.Append("(");
+                builder.Append('(');
 
                 builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)");
 
@@ -5239,7 +5240,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             {
                 if (i > 0)
                 {
-                    insertText.Append(",");
+                    insertText.Append(',');
                 }
 
                 insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture));
@@ -6332,7 +6333,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
                     foreach (var column in _mediaAttachmentSaveColumns.Skip(1))
                     {
-                        insertText.Append("@" + column + index + ",");
+                        insertText.Append('@')
+                            .Append(column)
+                            .Append(index)
+                            .Append(',');
                     }
 
                     insertText.Length -= 1;

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

@@ -34,10 +34,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.5" />
-    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.5" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" />
-    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.5" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.6" />
+    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
+    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" />
     <PackageReference Include="Mono.Nat" Version="2.0.1" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
     <PackageReference Include="ServiceStack.Text.Core" Version="5.9.0" />

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

@@ -1,3 +1,4 @@
+using System.Net.Sockets;
 using System.Threading;
 using System.Threading.Tasks;
 using Emby.Server.Implementations.Udp;
@@ -48,8 +49,16 @@ namespace Emby.Server.Implementations.EntryPoints
         /// <inheritdoc />
         public Task RunAsync()
         {
-            _udpServer = new UdpServer(_logger, _appHost, _config);
-            _udpServer.Start(PortNumber, _cancellationTokenSource.Token);
+            try
+            {
+                _udpServer = new UdpServer(_logger, _appHost, _config);
+                _udpServer.Start(PortNumber, _cancellationTokenSource.Token);
+            }
+            catch (SocketException ex)
+            {
+                _logger.LogWarning(ex, "Unable to start AutoDiscovery listener on UDP port {PortNumber}", PortNumber);
+            }
+
             return Task.CompletedTask;
         }
 

+ 10 - 0
Emby.Server.Implementations/IO/ManagedFileSystem.cs

@@ -245,6 +245,16 @@ namespace Emby.Server.Implementations.IO
                 if (info is FileInfo fileInfo)
                 {
                     result.Length = fileInfo.Length;
+
+                    // Issue #2354 get the size of files behind symbolic links
+                    if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
+                    {
+                        using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
+                        {
+                            result.Length = thisFileStream.Length;
+                        }
+                    }
+
                     result.DirectoryName = fileInfo.DirectoryName;
                 }
 

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

@@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library
                 if (parent != null)
                 {
                     // Don't resolve these into audio files
-                    if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename)
+                    if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename, StringComparison.Ordinal)
                         && _libraryManager.IsAudioFile(filename))
                     {
                         return true;

+ 12 - 12
Emby.Server.Implementations/Library/ExclusiveLiveStream.cs

@@ -11,6 +11,17 @@ namespace Emby.Server.Implementations.Library
 {
     public class ExclusiveLiveStream : ILiveStream
     {
+        private readonly Func<Task> _closeFn;
+
+        public ExclusiveLiveStream(MediaSourceInfo mediaSource, Func<Task> closeFn)
+        {
+            MediaSource = mediaSource;
+            EnableStreamSharing = false;
+            _closeFn = closeFn;
+            ConsumerCount = 1;
+            UniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+        }
+
         public int ConsumerCount { get; set; }
 
         public string OriginalStreamId { get; set; }
@@ -21,18 +32,7 @@ namespace Emby.Server.Implementations.Library
 
         public MediaSourceInfo MediaSource { get; set; }
 
-        public string UniqueId { get; private set; }
-
-        private Func<Task> _closeFn;
-
-        public ExclusiveLiveStream(MediaSourceInfo mediaSource, Func<Task> closeFn)
-        {
-            MediaSource = mediaSource;
-            EnableStreamSharing = false;
-            _closeFn = closeFn;
-            ConsumerCount = 1;
-            UniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
-        }
+        public string UniqueId { get; }
 
         public Task Close()
         {

+ 119 - 142
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -59,6 +59,8 @@ namespace Emby.Server.Implementations.Library
     /// </summary>
     public class LibraryManager : ILibraryManager
     {
+        private const string ShortcutFileExtension = ".mblink";
+
         private readonly ILogger<LibraryManager> _logger;
         private readonly ITaskManager _taskManager;
         private readonly IUserManager _userManager;
@@ -74,63 +76,24 @@ namespace Emby.Server.Implementations.Library
         private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
         private readonly IImageProcessor _imageProcessor;
 
-        private NamingOptions _namingOptions;
-        private string[] _videoFileExtensions;
-
-        private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
-
-        private IProviderManager ProviderManager => _providerManagerFactory.Value;
-
-        private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
-
-        /// <summary>
-        /// Gets or sets the postscan tasks.
-        /// </summary>
-        /// <value>The postscan tasks.</value>
-        private ILibraryPostScanTask[] PostscanTasks { get; set; }
-
-        /// <summary>
-        /// Gets or sets the intro providers.
-        /// </summary>
-        /// <value>The intro providers.</value>
-        private IIntroProvider[] IntroProviders { get; set; }
-
-        /// <summary>
-        /// Gets or sets the list of entity resolution ignore rules.
-        /// </summary>
-        /// <value>The entity resolution ignore rules.</value>
-        private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
-
-        /// <summary>
-        /// Gets or sets the list of currently registered entity resolvers.
-        /// </summary>
-        /// <value>The entity resolvers enumerable.</value>
-        private IItemResolver[] EntityResolvers { get; set; }
-
-        private IMultiItemResolver[] MultiItemResolvers { get; set; }
-
         /// <summary>
-        /// Gets or sets the comparers.
+        /// The _root folder sync lock.
         /// </summary>
-        /// <value>The comparers.</value>
-        private IBaseItemComparer[] Comparers { get; set; }
+        private readonly object _rootFolderSyncLock = new object();
+        private readonly object _userRootFolderSyncLock = new object();
 
-        /// <summary>
-        /// Occurs when [item added].
-        /// </summary>
-        public event EventHandler<ItemChangeEventArgs> ItemAdded;
+        private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
 
-        /// <summary>
-        /// Occurs when [item updated].
-        /// </summary>
-        public event EventHandler<ItemChangeEventArgs> ItemUpdated;
+        private NamingOptions _namingOptions;
+        private string[] _videoFileExtensions;
 
         /// <summary>
-        /// Occurs when [item removed].
+        /// The _root folder.
         /// </summary>
-        public event EventHandler<ItemChangeEventArgs> ItemRemoved;
+        private volatile AggregateFolder _rootFolder;
+        private volatile UserRootFolder _userRootFolder;
 
-        public bool IsScanRunning { get; private set; }
+        private bool _wizardCompleted;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="LibraryManager" /> class.
@@ -185,37 +148,19 @@ namespace Emby.Server.Implementations.Library
         }
 
         /// <summary>
-        /// Adds the parts.
+        /// Occurs when [item added].
         /// </summary>
-        /// <param name="rules">The rules.</param>
-        /// <param name="resolvers">The resolvers.</param>
-        /// <param name="introProviders">The intro providers.</param>
-        /// <param name="itemComparers">The item comparers.</param>
-        /// <param name="postscanTasks">The post scan tasks.</param>
-        public void AddParts(
-            IEnumerable<IResolverIgnoreRule> rules,
-            IEnumerable<IItemResolver> resolvers,
-            IEnumerable<IIntroProvider> introProviders,
-            IEnumerable<IBaseItemComparer> itemComparers,
-            IEnumerable<ILibraryPostScanTask> postscanTasks)
-        {
-            EntityResolutionIgnoreRules = rules.ToArray();
-            EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
-            MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
-            IntroProviders = introProviders.ToArray();
-            Comparers = itemComparers.ToArray();
-            PostscanTasks = postscanTasks.ToArray();
-        }
+        public event EventHandler<ItemChangeEventArgs> ItemAdded;
 
         /// <summary>
-        /// The _root folder.
+        /// Occurs when [item updated].
         /// </summary>
-        private volatile AggregateFolder _rootFolder;
+        public event EventHandler<ItemChangeEventArgs> ItemUpdated;
 
         /// <summary>
-        /// The _root folder sync lock.
+        /// Occurs when [item removed].
         /// </summary>
-        private readonly object _rootFolderSyncLock = new object();
+        public event EventHandler<ItemChangeEventArgs> ItemRemoved;
 
         /// <summary>
         /// Gets the root folder.
@@ -240,7 +185,68 @@ namespace Emby.Server.Implementations.Library
             }
         }
 
-        private bool _wizardCompleted;
+        private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
+
+        private IProviderManager ProviderManager => _providerManagerFactory.Value;
+
+        private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
+
+        /// <summary>
+        /// Gets or sets the postscan tasks.
+        /// </summary>
+        /// <value>The postscan tasks.</value>
+        private ILibraryPostScanTask[] PostscanTasks { get; set; }
+
+        /// <summary>
+        /// Gets or sets the intro providers.
+        /// </summary>
+        /// <value>The intro providers.</value>
+        private IIntroProvider[] IntroProviders { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of entity resolution ignore rules.
+        /// </summary>
+        /// <value>The entity resolution ignore rules.</value>
+        private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of currently registered entity resolvers.
+        /// </summary>
+        /// <value>The entity resolvers enumerable.</value>
+        private IItemResolver[] EntityResolvers { get; set; }
+
+        private IMultiItemResolver[] MultiItemResolvers { get; set; }
+
+        /// <summary>
+        /// Gets or sets the comparers.
+        /// </summary>
+        /// <value>The comparers.</value>
+        private IBaseItemComparer[] Comparers { get; set; }
+
+        public bool IsScanRunning { get; private set; }
+
+        /// <summary>
+        /// Adds the parts.
+        /// </summary>
+        /// <param name="rules">The rules.</param>
+        /// <param name="resolvers">The resolvers.</param>
+        /// <param name="introProviders">The intro providers.</param>
+        /// <param name="itemComparers">The item comparers.</param>
+        /// <param name="postscanTasks">The post scan tasks.</param>
+        public void AddParts(
+            IEnumerable<IResolverIgnoreRule> rules,
+            IEnumerable<IItemResolver> resolvers,
+            IEnumerable<IIntroProvider> introProviders,
+            IEnumerable<IBaseItemComparer> itemComparers,
+            IEnumerable<ILibraryPostScanTask> postscanTasks)
+        {
+            EntityResolutionIgnoreRules = rules.ToArray();
+            EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
+            MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
+            IntroProviders = introProviders.ToArray();
+            Comparers = itemComparers.ToArray();
+            PostscanTasks = postscanTasks.ToArray();
+        }
 
         /// <summary>
         /// Records the configuration values.
@@ -340,7 +346,7 @@ namespace Emby.Server.Implementations.Library
             if (item is LiveTvProgram)
             {
                 _logger.LogDebug(
-                    "Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+                    "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
                     item.GetType().Name,
                     item.Name ?? "Unknown name",
                     item.Path ?? string.Empty,
@@ -349,7 +355,7 @@ namespace Emby.Server.Implementations.Library
             else
             {
                 _logger.LogInformation(
-                    "Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+                    "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
                     item.GetType().Name,
                     item.Name ?? "Unknown name",
                     item.Path ?? string.Empty,
@@ -367,7 +373,12 @@ namespace Emby.Server.Implementations.Library
                     continue;
                 }
 
-                _logger.LogDebug("Deleting path {MetadataPath}", metadataPath);
+                _logger.LogDebug(
+                    "Deleting metadata path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+                    item.GetType().Name,
+                    item.Name ?? "Unknown name",
+                    metadataPath,
+                    item.Id);
 
                 try
                 {
@@ -391,7 +402,13 @@ namespace Emby.Server.Implementations.Library
                     {
                         try
                         {
-                            _logger.LogDebug("Deleting path {path}", fileSystemInfo.FullName);
+                            _logger.LogInformation(
+                                "Deleting item path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+                                item.GetType().Name,
+                                item.Name ?? "Unknown name",
+                                fileSystemInfo.FullName,
+                                item.Id);
+
                             if (fileSystemInfo.IsDirectory)
                             {
                                 Directory.Delete(fileSystemInfo.FullName, true);
@@ -500,7 +517,7 @@ namespace Emby.Server.Implementations.Library
                 // Try to normalize paths located underneath program-data in an attempt to make them more portable
                 key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length)
                     .TrimStart(new[] { '/', '\\' })
-                    .Replace("/", "\\");
+                    .Replace('/', '\\');
             }
 
             if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
@@ -763,14 +780,11 @@ namespace Emby.Server.Implementations.Library
             return rootFolder;
         }
 
-        private volatile UserRootFolder _userRootFolder;
-        private readonly object _syncLock = new object();
-
         public Folder GetUserRootFolder()
         {
             if (_userRootFolder == null)
             {
-                lock (_syncLock)
+                lock (_userRootFolderSyncLock)
                 {
                     if (_userRootFolder == null)
                     {
@@ -1320,7 +1334,7 @@ namespace Emby.Server.Implementations.Library
 
             return new QueryResult<BaseItem>
             {
-                Items = _itemRepository.GetItemList(query).ToArray()
+                Items = _itemRepository.GetItemList(query)
             };
         }
 
@@ -1451,11 +1465,9 @@ namespace Emby.Server.Implementations.Library
                 return _itemRepository.GetItems(query);
             }
 
-            var list = _itemRepository.GetItemList(query);
-
             return new QueryResult<BaseItem>
             {
-                Items = list
+                Items = _itemRepository.GetItemList(query)
             };
         }
 
@@ -1864,7 +1876,8 @@ namespace Emby.Server.Implementations.Library
             }
 
             var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
-            if (outdated.Length == 0)
+            // Skip image processing if current or live tv source
+            if (outdated.Length == 0 || item.SourceType != SourceType.Library)
             {
                 RegisterItem(item);
                 return;
@@ -1933,12 +1946,9 @@ namespace Emby.Server.Implementations.Library
         /// <summary>
         /// Updates the item.
         /// </summary>
-        public void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
+        public void UpdateItems(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
         {
-            // Don't iterate multiple times
-            var itemsList = items.ToList();
-
-            foreach (var item in itemsList)
+            foreach (var item in items)
             {
                 if (item.IsFileProtocol)
                 {
@@ -1950,11 +1960,11 @@ namespace Emby.Server.Implementations.Library
                 UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate);
             }
 
-            _itemRepository.SaveItems(itemsList, cancellationToken);
+            _itemRepository.SaveItems(items, cancellationToken);
 
             if (ItemUpdated != null)
             {
-                foreach (var item in itemsList)
+                foreach (var item in items)
                 {
                     // With the live tv guide this just creates too much noise
                     if (item.SourceType != SourceType.Library)
@@ -2177,8 +2187,6 @@ namespace Emby.Server.Implementations.Library
                 .FirstOrDefault(i => !string.IsNullOrEmpty(i));
         }
 
-        private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
-
         public UserView GetNamedView(
             User user,
             string name,
@@ -2476,14 +2484,9 @@ namespace Emby.Server.Implementations.Library
 
             var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
 
-            var episodeInfo = episode.IsFileProtocol ?
-                resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) :
-                new Naming.TV.EpisodeInfo();
-
-            if (episodeInfo == null)
-            {
-                episodeInfo = new Naming.TV.EpisodeInfo();
-            }
+            var episodeInfo = episode.IsFileProtocol
+                ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo()
+                : new Naming.TV.EpisodeInfo();
 
             try
             {
@@ -2491,11 +2494,13 @@ namespace Emby.Server.Implementations.Library
                 if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase))
                 {
                     // Read from metadata
-                    var mediaInfo = _mediaEncoder.GetMediaInfo(new MediaInfoRequest
-                    {
-                        MediaSource = episode.GetMediaSources(false)[0],
-                        MediaType = DlnaProfileType.Video
-                    }, CancellationToken.None).GetAwaiter().GetResult();
+                    var mediaInfo = _mediaEncoder.GetMediaInfo(
+                        new MediaInfoRequest
+                        {
+                            MediaSource = episode.GetMediaSources(false)[0],
+                            MediaType = DlnaProfileType.Video
+                        },
+                        CancellationToken.None).GetAwaiter().GetResult();
                     if (mediaInfo.ParentIndexNumber > 0)
                     {
                         episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
@@ -2653,7 +2658,7 @@ namespace Emby.Server.Implementations.Library
 
             var videos = videoListResolver.Resolve(fileSystemChildren);
 
-            var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase));
+            var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
 
             if (currentVideo != null)
             {
@@ -2670,9 +2675,7 @@ namespace Emby.Server.Implementations.Library
                 .Select(video =>
                 {
                     // Try to retrieve it from the db. If we don't find it, use the resolved version
-                    var dbItem = GetItemById(video.Id) as Trailer;
-
-                    if (dbItem != null)
+                    if (GetItemById(video.Id) is Trailer dbItem)
                     {
                         video = dbItem;
                     }
@@ -2999,23 +3002,6 @@ namespace Emby.Server.Implementations.Library
             });
         }
 
-        private static bool ValidateNetworkPath(string path)
-        {
-            // if (Environment.OSVersion.Platform == PlatformID.Win32NT)
-            //{
-            //    // We can't validate protocol-based paths, so just allow them
-            //    if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) == -1)
-            //    {
-            //        return Directory.Exists(path);
-            //    }
-            //}
-
-            // Without native support for unc, we cannot validate this when running under mono
-            return true;
-        }
-
-        private const string ShortcutFileExtension = ".mblink";
-
         public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
         {
             AddMediaPathInternal(virtualFolderName, pathInfo, true);
@@ -3040,11 +3026,6 @@ namespace Emby.Server.Implementations.Library
                 throw new FileNotFoundException("The path does not exist.");
             }
 
-            if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
-            {
-                throw new FileNotFoundException("The network path does not exist.");
-            }
-
             var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
             var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 
@@ -3083,11 +3064,6 @@ namespace Emby.Server.Implementations.Library
                 throw new ArgumentNullException(nameof(pathInfo));
             }
 
-            if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
-            {
-                throw new FileNotFoundException("The network path does not exist.");
-            }
-
             var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
             var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 
@@ -3219,7 +3195,8 @@ namespace Emby.Server.Implementations.Library
 
             if (!Directory.Exists(virtualFolderPath))
             {
-                throw new FileNotFoundException(string.Format("The media collection {0} does not exist", virtualFolderName));
+                throw new FileNotFoundException(
+                    string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolderName));
             }
 
             var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)

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

@@ -23,9 +23,8 @@ namespace Emby.Server.Implementations.Library
     {
         private readonly IMediaEncoder _mediaEncoder;
         private readonly ILogger _logger;
-
-        private IJsonSerializer _json;
-        private IApplicationPaths _appPaths;
+        private readonly IJsonSerializer _json;
+        private readonly IApplicationPaths _appPaths;
 
         public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IJsonSerializer json, IApplicationPaths appPaths)
         {
@@ -72,13 +71,14 @@ namespace Emby.Server.Implementations.Library
 
                 mediaSource.AnalyzeDurationMs = 3000;
 
-                mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
-                {
-                    MediaSource = mediaSource,
-                    MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
-                    ExtractChapters = false
-
-                }, cancellationToken).ConfigureAwait(false);
+                mediaInfo = await _mediaEncoder.GetMediaInfo(
+                    new MediaInfoRequest
+                    {
+                        MediaSource = mediaSource,
+                        MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
+                        ExtractChapters = false
+                    },
+                    cancellationToken).ConfigureAwait(false);
 
                 if (cacheFilePath != null)
                 {
@@ -126,7 +126,7 @@ namespace Emby.Server.Implementations.Library
                 mediaSource.RunTimeTicks = null;
             }
 
-            var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio);
+            var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
 
             if (audioStream == null || audioStream.Index == -1)
             {
@@ -137,7 +137,7 @@ namespace Emby.Server.Implementations.Library
                 mediaSource.DefaultAudioStreamIndex = audioStream.Index;
             }
 
-            var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Video);
+            var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
             if (videoStream != null)
             {
                 if (!videoStream.BitRate.HasValue)

+ 9 - 8
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -29,6 +29,9 @@ namespace Emby.Server.Implementations.Library
 {
     public class MediaSourceManager : IMediaSourceManager, IDisposable
     {
+        // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
+        private const char LiveStreamIdDelimeter = '_';
+
         private readonly IItemRepository _itemRepo;
         private readonly IUserManager _userManager;
         private readonly ILibraryManager _libraryManager;
@@ -40,6 +43,11 @@ namespace Emby.Server.Implementations.Library
         private readonly ILocalizationManager _localizationManager;
         private readonly IApplicationPaths _appPaths;
 
+        private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
+        private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
+
+        private readonly object _disposeLock = new object();
+
         private IMediaSourceProvider[] _providers;
 
         public MediaSourceManager(
@@ -368,7 +376,6 @@ namespace Emby.Server.Implementations.Library
                 }
             }
 
-
             var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
                 ? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
 
@@ -451,9 +458,6 @@ namespace Emby.Server.Implementations.Library
             .ToList();
         }
 
-        private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
-        private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
-
         public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
         {
             await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
@@ -855,9 +859,6 @@ namespace Emby.Server.Implementations.Library
             }
         }
 
-        // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
-        private const char LiveStreamIdDelimeter = '_';
-
         private Tuple<IMediaSourceProvider, string> GetProvider(string key)
         {
             if (string.IsNullOrEmpty(key))
@@ -881,9 +882,9 @@ namespace Emby.Server.Implementations.Library
         public void Dispose()
         {
             Dispose(true);
+            GC.SuppressFinalize(this);
         }
 
-        private readonly object _disposeLock = new object();
         /// <summary>
         /// Releases unmanaged and - optionally - managed resources.
         /// </summary>

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

@@ -89,7 +89,7 @@ namespace Emby.Server.Implementations.Library
             }
 
             // load forced subs if we have found no suitable full subtitles
-            stream = stream ?? streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase));
+            stream ??= streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase));
 
             if (stream != null)
             {

+ 6 - 12
Emby.Server.Implementations/Library/SearchEngine.cs

@@ -20,13 +20,11 @@ namespace Emby.Server.Implementations.Library
 {
     public class SearchEngine : ISearchEngine
     {
-        private readonly ILogger<SearchEngine> _logger;
         private readonly ILibraryManager _libraryManager;
         private readonly IUserManager _userManager;
 
-        public SearchEngine(ILogger<SearchEngine> logger, ILibraryManager libraryManager, IUserManager userManager)
+        public SearchEngine(ILibraryManager libraryManager, IUserManager userManager)
         {
-            _logger = logger;
             _libraryManager = libraryManager;
             _userManager = userManager;
         }
@@ -34,11 +32,7 @@ namespace Emby.Server.Implementations.Library
         public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query)
         {
             User user = null;
-
-            if (query.UserId.Equals(Guid.Empty))
-            {
-            }
-            else
+            if (query.UserId != Guid.Empty)
             {
                 user = _userManager.GetUserById(query.UserId);
             }
@@ -48,19 +42,19 @@ namespace Emby.Server.Implementations.Library
 
             if (query.StartIndex.HasValue)
             {
-                results = results.Skip(query.StartIndex.Value).ToList();
+                results = results.GetRange(query.StartIndex.Value, totalRecordCount - query.StartIndex.Value);
             }
 
             if (query.Limit.HasValue)
             {
-                results = results.Take(query.Limit.Value).ToList();
+                results = results.GetRange(0, query.Limit.Value);
             }
 
             return new QueryResult<SearchHintInfo>
             {
                 TotalRecordCount = totalRecordCount,
 
-                Items = results.ToArray()
+                Items = results
             };
         }
 
@@ -85,7 +79,7 @@ namespace Emby.Server.Implementations.Library
 
             if (string.IsNullOrEmpty(searchTerm))
             {
-                throw new ArgumentNullException("SearchTerm can't be empty.", nameof(searchTerm));
+                throw new ArgumentException("SearchTerm can't be empty.", nameof(query));
             }
 
             searchTerm = searchTerm.Trim().RemoveDiacritics();

+ 3 - 6
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -4,6 +4,7 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using System.Text.Json;
 using System.Threading;
 using System.Threading.Tasks;
 using Emby.Server.Implementations.Library;
@@ -28,7 +29,6 @@ using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.Logging;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
@@ -54,7 +54,6 @@ namespace Emby.Server.Implementations.LiveTv
         private readonly ILibraryManager _libraryManager;
         private readonly ITaskManager _taskManager;
         private readonly ILocalizationManager _localization;
-        private readonly IJsonSerializer _jsonSerializer;
         private readonly IFileSystem _fileSystem;
         private readonly IChannelManager _channelManager;
         private readonly LiveTvDtoService _tvDtoService;
@@ -73,7 +72,6 @@ namespace Emby.Server.Implementations.LiveTv
             ILibraryManager libraryManager,
             ITaskManager taskManager,
             ILocalizationManager localization,
-            IJsonSerializer jsonSerializer,
             IFileSystem fileSystem,
             IChannelManager channelManager,
             LiveTvDtoService liveTvDtoService)
@@ -85,7 +83,6 @@ namespace Emby.Server.Implementations.LiveTv
             _libraryManager = libraryManager;
             _taskManager = taskManager;
             _localization = localization;
-            _jsonSerializer = jsonSerializer;
             _fileSystem = fileSystem;
             _dtoService = dtoService;
             _userDataManager = userDataManager;
@@ -2234,7 +2231,7 @@ namespace Emby.Server.Implementations.LiveTv
 
         public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)
         {
-            info = _jsonSerializer.DeserializeFromString<TunerHostInfo>(_jsonSerializer.SerializeToString(info));
+            info = JsonSerializer.Deserialize<TunerHostInfo>(JsonSerializer.Serialize(info));
 
             var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
 
@@ -2278,7 +2275,7 @@ namespace Emby.Server.Implementations.LiveTv
         {
             // Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider
             // ServerConfiguration.SaveConfiguration crashes during xml serialization for AddListingProvider
-            info = _jsonSerializer.DeserializeFromString<ListingsProviderInfo>(_jsonSerializer.SerializeToString(info));
+            info = JsonSerializer.Deserialize<ListingsProviderInfo>(JsonSerializer.Serialize(info));
 
             var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
 

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

@@ -3,7 +3,7 @@
     "AppDeviceValues": "App: {0}, Gerät: {1}",
     "Application": "Anwendung",
     "Artists": "Interpreten",
-    "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifiziert",
+    "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich angemeldet",
     "Books": "Bücher",
     "CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
     "Channels": "Kanäle",

+ 32 - 32
Emby.Server.Implementations/Localization/Core/ms.json

@@ -5,47 +5,47 @@
     "Artists": "Artis",
     "AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
     "Books": "Buku-buku",
-    "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
+    "CameraImageUploadedFrom": "Ada gambar dari kamera yang baru dimuat naik melalui {0}",
     "Channels": "Saluran",
-    "ChapterNameValue": "Chapter {0}",
+    "ChapterNameValue": "Bab {0}",
     "Collections": "Koleksi",
-    "DeviceOfflineWithName": "{0} has disconnected",
-    "DeviceOnlineWithName": "{0} is connected",
+    "DeviceOfflineWithName": "{0} telah diputuskan sambungan",
+    "DeviceOnlineWithName": "{0} telah disambung",
     "FailedLoginAttemptWithUserName": "Cubaan log masuk gagal dari {0}",
-    "Favorites": "Favorites",
-    "Folders": "Folders",
+    "Favorites": "Kegemaran",
+    "Folders": "Fail-fail",
     "Genres": "Genre-genre",
-    "HeaderAlbumArtists": "Album Artists",
+    "HeaderAlbumArtists": "Album Artis-artis",
     "HeaderCameraUploads": "Muatnaik Kamera",
     "HeaderContinueWatching": "Terus Menonton",
-    "HeaderFavoriteAlbums": "Favorite Albums",
-    "HeaderFavoriteArtists": "Favorite Artists",
-    "HeaderFavoriteEpisodes": "Favorite Episodes",
-    "HeaderFavoriteShows": "Favorite Shows",
-    "HeaderFavoriteSongs": "Favorite Songs",
-    "HeaderLiveTV": "Live TV",
-    "HeaderNextUp": "Next Up",
-    "HeaderRecordingGroups": "Recording Groups",
-    "HomeVideos": "Home videos",
-    "Inherit": "Inherit",
-    "ItemAddedWithName": "{0} was added to the library",
-    "ItemRemovedWithName": "{0} was removed from the library",
+    "HeaderFavoriteAlbums": "Album-album Kegemaran",
+    "HeaderFavoriteArtists": "Artis-artis Kegemaran",
+    "HeaderFavoriteEpisodes": "Episod-episod Kegemaran",
+    "HeaderFavoriteShows": "Rancangan-rancangan Kegemaran",
+    "HeaderFavoriteSongs": "Lagu-lagu Kegemaran",
+    "HeaderLiveTV": "TV Siaran Langsung",
+    "HeaderNextUp": "Seterusnya",
+    "HeaderRecordingGroups": "Kumpulan-kumpulan Rakaman",
+    "HomeVideos": "Video Personal",
+    "Inherit": "Mewarisi",
+    "ItemAddedWithName": "{0} telah ditambahkan ke dalam pustaka",
+    "ItemRemovedWithName": "{0} telah dibuang daripada pustaka",
     "LabelIpAddressValue": "Alamat IP: {0}",
-    "LabelRunningTimeValue": "Running time: {0}",
-    "Latest": "Latest",
-    "MessageApplicationUpdated": "Jellyfin Server has been updated",
-    "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
-    "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated",
-    "MessageServerConfigurationUpdated": "Server configuration has been updated",
-    "MixedContent": "Mixed content",
-    "Movies": "Movies",
+    "LabelRunningTimeValue": "Masa berjalan: {0}",
+    "Latest": "Terbaru",
+    "MessageApplicationUpdated": "Jellyfin Server telah dikemas kini",
+    "MessageApplicationUpdatedTo": "Jellyfin Server telah dikemas kini ke {0}",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan di bahagian {0} telah dikemas kini",
+    "MessageServerConfigurationUpdated": "Konfigurasi pelayan telah dikemas kini",
+    "MixedContent": "Kandungan campuran",
+    "Movies": "Filem",
     "Music": "Muzik",
     "MusicVideos": "Video muzik",
-    "NameInstallFailed": "{0} installation failed",
-    "NameSeasonNumber": "Season {0}",
-    "NameSeasonUnknown": "Season Unknown",
-    "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
-    "NotificationOptionApplicationUpdateAvailable": "Application update available",
+    "NameInstallFailed": "{0} pemasangan gagal",
+    "NameSeasonNumber": "Musim {0}",
+    "NameSeasonUnknown": "Musim Tidak Diketahui",
+    "NewVersionIsAvailable": "Versi terbaru Jellyfin Server bersedia untuk dimuat turunkan.",
+    "NotificationOptionApplicationUpdateAvailable": "Kemas kini aplikasi telah sedia",
     "NotificationOptionApplicationUpdateInstalled": "Application update installed",
     "NotificationOptionAudioPlayback": "Audio playback started",
     "NotificationOptionAudioPlaybackStopped": "Audio playback stopped",

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

@@ -67,5 +67,7 @@
     "Artists": "นักแสดง",
     "Application": "แอปพลิเคชั่น",
     "AppDeviceValues": "App: {0}, อุปกรณ์: {1}",
-    "Albums": "อัลบั้ม"
+    "Albums": "อัลบั้ม",
+    "ScheduledTaskStartedWithName": "{0} เริ่มต้น",
+    "ScheduledTaskFailedWithName": "{0} ล้มเหลว"
 }

+ 12 - 1
Emby.Server.Implementations/Networking/NetworkManager.cs

@@ -152,7 +152,12 @@ namespace Emby.Server.Implementations.Networking
                 return true;
             }
 
-            byte[] octet = IPAddress.Parse(endpoint).GetAddressBytes();
+            if (!IPAddress.TryParse(endpoint, out var ipAddress))
+            {
+                return false;
+            }
+
+            byte[] octet = ipAddress.GetAddressBytes();
 
             if ((octet[0] == 10) ||
                 (octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918
@@ -268,6 +273,12 @@ namespace Emby.Server.Implementations.Networking
             string excludeAddress = "[" + addressString + "]";
             var subnets = LocalSubnetsFn();
 
+            // Include any address if LAN subnets aren't specified
+            if (subnets.Length == 0)
+            {
+                return true;
+            }
+
             // Exclude any addresses if they appear in the LAN list in [ ]
             if (Array.IndexOf(subnets, excludeAddress) != -1)
             {

+ 0 - 1
Emby.Server.Implementations/Services/ServiceController.cs

@@ -189,5 +189,4 @@ namespace Emby.Server.Implementations.Services
             return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName());
         }
     }
-
 }

+ 3 - 1
Emby.Server.Implementations/Services/ServiceHandler.cs

@@ -6,6 +6,7 @@ using System.Reflection;
 using System.Threading;
 using System.Threading.Tasks;
 using Emby.Server.Implementations.HttpServer;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Model.Services;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
@@ -78,7 +79,8 @@ namespace Emby.Server.Implementations.Services
             var request = await CreateRequest(httpHost, httpReq, _restPath, logger).ConfigureAwait(false);
 
             httpHost.ApplyRequestFilters(httpReq, httpRes, request);
-
+            
+            httpRes.HttpContext.SetServiceStackRequest(httpReq);
             var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
 
             // Apply response filters

+ 4 - 2
Emby.Server.Implementations/Services/ServicePath.cs

@@ -488,7 +488,8 @@ namespace Emby.Server.Implementations.Services
                         sb.Append(value);
                         for (var j = pathIx + 1; j < requestComponents.Length; j++)
                         {
-                            sb.Append(PathSeperatorChar + requestComponents[j]);
+                            sb.Append(PathSeperatorChar)
+                                .Append(requestComponents[j]);
                         }
 
                         value = sb.ToString();
@@ -505,7 +506,8 @@ namespace Emby.Server.Implementations.Services
                             pathIx++;
                             while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
                             {
-                                sb.Append(PathSeperatorChar + requestComponents[pathIx++]);
+                                sb.Append(PathSeperatorChar)
+                                    .Append(requestComponents[pathIx++]);
                             }
 
                             value = sb.ToString();

+ 13 - 16
Emby.Server.Implementations/TV/TVSeriesManager.cs

@@ -117,23 +117,20 @@ namespace Emby.Server.Implementations.TV
                 limit = limit.Value + 10;
             }
 
-            var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
-            {
-                IncludeItemTypes = new[] { typeof(Episode).Name },
-                OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) },
-                SeriesPresentationUniqueKey = presentationUniqueKey,
-                Limit = limit,
-                DtoOptions = new DtoOptions
-                {
-                    Fields = new ItemFields[]
+            var items = _libraryManager
+                .GetItemList(
+                    new InternalItemsQuery(user)
                     {
-                        ItemFields.SeriesPresentationUniqueKey
-                    },
-                    EnableImages = false
-                },
-                GroupBySeriesPresentationUniqueKey = true
-
-            }, parentsFolders.ToList()).Cast<Episode>().Select(GetUniqueSeriesKey);
+                        IncludeItemTypes = new[] { typeof(Episode).Name },
+                        OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) },
+                        SeriesPresentationUniqueKey = presentationUniqueKey,
+                        Limit = limit,
+                        DtoOptions = new DtoOptions { Fields = new[] { ItemFields.SeriesPresentationUniqueKey }, EnableImages = false },
+                        GroupBySeriesPresentationUniqueKey = true
+                    }, parentsFolders.ToList())
+                .Cast<Episode>()
+                .Where(episode => !string.IsNullOrEmpty(episode.SeriesPresentationUniqueKey))
+                .Select(GetUniqueSeriesKey);
 
             // Avoid implicitly captured closure
             var episodes = GetNextUpEpisodes(request, user, items, dtoOptions);

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

@@ -148,6 +148,11 @@ namespace Emby.Server.Implementations.Updates
                 _logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
                 return Array.Empty<PackageInfo>();
             }
+            catch (HttpRequestException ex)
+            {
+                _logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
+                return Array.Empty<PackageInfo>();
+            }
         }
 
         /// <inheritdoc />

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

@@ -46,14 +46,12 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("Configuration")]
         public StartupConfigurationDto GetStartupConfiguration()
         {
-            var result = new StartupConfigurationDto
+            return new StartupConfigurationDto
             {
                 UICulture = _config.Configuration.UICulture,
                 MetadataCountryCode = _config.Configuration.MetadataCountryCode,
                 PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage
             };
-
-            return result;
         }
 
         /// <summary>
@@ -92,10 +90,10 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <returns>The first user.</returns>
         [HttpGet("User")]
-        public StartupUserDto GetFirstUser()
+        public async Task<StartupUserDto> GetFirstUser()
         {
             // TODO: Remove this method when startup wizard no longer requires an existing user.
-            _userManager.Initialize();
+            await _userManager.InitializeAsync().ConfigureAwait(false);
             var user = _userManager.Users.First();
             return new StartupUserDto
             {

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

@@ -14,7 +14,7 @@
 
   <ItemGroup>
     <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
-    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.5" />
+    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.6" />
     <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
   </ItemGroup>

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

@@ -19,8 +19,8 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.5" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.5" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.6" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.6" />
   </ItemGroup>
 
 </Project>

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

@@ -24,11 +24,11 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.5">
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.6">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.5">
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.6">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>

+ 13 - 0
Jellyfin.Server.Implementations/JellyfinDb.cs

@@ -1,5 +1,6 @@
 #pragma warning disable CS1591
 
+using System;
 using System.Linq;
 using Jellyfin.Data;
 using Jellyfin.Data.Entities;
@@ -135,6 +136,18 @@ namespace Jellyfin.Server.Implementations
             return base.SaveChanges();
         }
 
+        /// <inheritdoc/>
+        public override void Dispose()
+        {
+            foreach (var entry in ChangeTracker.Entries())
+            {
+                entry.State = EntityState.Detached;
+            }
+
+            GC.SuppressFinalize(this);
+            base.Dispose();
+        }
+
         /// <inheritdoc />
         protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
         {

+ 9 - 7
Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs

@@ -6,6 +6,7 @@ using System.IO;
 using System.Security.Cryptography;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
+using MediaBrowser.Common;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Configuration;
@@ -23,7 +24,7 @@ namespace Jellyfin.Server.Implementations.Users
         private const string BaseResetFileName = "passwordreset";
 
         private readonly IJsonSerializer _jsonSerializer;
-        private readonly IUserManager _userManager;
+        private readonly IApplicationHost _appHost;
 
         private readonly string _passwordResetFileBase;
         private readonly string _passwordResetFileBaseDir;
@@ -33,16 +34,17 @@ namespace Jellyfin.Server.Implementations.Users
         /// </summary>
         /// <param name="configurationManager">The configuration manager.</param>
         /// <param name="jsonSerializer">The JSON serializer.</param>
-        /// <param name="userManager">The user manager.</param>
+        /// <param name="appHost">The application host.</param>
         public DefaultPasswordResetProvider(
             IServerConfigurationManager configurationManager,
             IJsonSerializer jsonSerializer,
-            IUserManager userManager)
+            IApplicationHost appHost)
         {
             _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath;
             _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName);
             _jsonSerializer = jsonSerializer;
-            _userManager = userManager;
+            _appHost = appHost;
+            // TODO: Remove the circular dependency on UserManager
         }
 
         /// <inheritdoc />
@@ -54,6 +56,7 @@ namespace Jellyfin.Server.Implementations.Users
         /// <inheritdoc />
         public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
         {
+            var userManager = _appHost.Resolve<IUserManager>();
             var usersReset = new List<string>();
             foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
             {
@@ -72,10 +75,10 @@ namespace Jellyfin.Server.Implementations.Users
                     pin.Replace("-", string.Empty, StringComparison.Ordinal),
                     StringComparison.InvariantCultureIgnoreCase))
                 {
-                    var resetUser = _userManager.GetUserByName(spr.UserName)
+                    var resetUser = userManager.GetUserByName(spr.UserName)
                         ?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found");
 
-                    await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
+                    await userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
                     usersReset.Add(resetUser.Username);
                     File.Delete(resetFile);
                 }
@@ -121,7 +124,6 @@ namespace Jellyfin.Server.Implementations.Users
             }
 
             user.EasyPassword = pin;
-            await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
 
             return new ForgotPasswordResult
             {

+ 66 - 43
Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -39,12 +39,11 @@ namespace Jellyfin.Server.Implementations.Users
         private readonly IApplicationHost _appHost;
         private readonly IImageProcessor _imageProcessor;
         private readonly ILogger<UserManager> _logger;
-
-        private IAuthenticationProvider[] _authenticationProviders = null!;
-        private DefaultAuthenticationProvider _defaultAuthenticationProvider = null!;
-        private InvalidAuthProvider _invalidAuthProvider = null!;
-        private IPasswordResetProvider[] _passwordResetProviders = null!;
-        private DefaultPasswordResetProvider _defaultPasswordResetProvider = null!;
+        private readonly IReadOnlyCollection<IPasswordResetProvider> _passwordResetProviders;
+        private readonly IReadOnlyCollection<IAuthenticationProvider> _authenticationProviders;
+        private readonly InvalidAuthProvider _invalidAuthProvider;
+        private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider;
+        private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="UserManager"/> class.
@@ -69,6 +68,13 @@ namespace Jellyfin.Server.Implementations.Users
             _appHost = appHost;
             _imageProcessor = imageProcessor;
             _logger = logger;
+
+            _passwordResetProviders = appHost.GetExports<IPasswordResetProvider>();
+            _authenticationProviders = appHost.GetExports<IAuthenticationProvider>();
+
+            _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
+            _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
+            _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
         }
 
         /// <inheritdoc/>
@@ -102,7 +108,16 @@ namespace Jellyfin.Server.Implementations.Users
         }
 
         /// <inheritdoc/>
-        public IEnumerable<Guid> UsersIds => _dbProvider.CreateContext().Users.Select(u => u.Id);
+        public IEnumerable<Guid> UsersIds
+        {
+            get
+            {
+                using var dbContext = _dbProvider.CreateContext();
+                return dbContext.Users
+                    .Select(user => user.Id)
+                    .ToList();
+            }
+        }
 
         /// <inheritdoc/>
         public User? GetUserById(Guid id)
@@ -152,12 +167,12 @@ namespace Jellyfin.Server.Implementations.Users
                 throw new ArgumentException("Invalid username", nameof(newName));
             }
 
-            if (user.Username.Equals(newName, StringComparison.OrdinalIgnoreCase))
+            if (user.Username.Equals(newName, StringComparison.Ordinal))
             {
                 throw new ArgumentException("The new and old names must be different.");
             }
 
-            if (Users.Any(u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.OrdinalIgnoreCase)))
+            if (Users.Any(u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.Ordinal)))
             {
                 throw new ArgumentException(string.Format(
                     CultureInfo.InvariantCulture,
@@ -188,8 +203,24 @@ namespace Jellyfin.Server.Implementations.Users
             await dbContext.SaveChangesAsync().ConfigureAwait(false);
         }
 
+        internal async Task<User> CreateUserInternalAsync(string name, JellyfinDb dbContext)
+        {
+            // TODO: Remove after user item data is migrated.
+            var max = await dbContext.Users.AnyAsync().ConfigureAwait(false)
+                ? await dbContext.Users.Select(u => u.InternalId).MaxAsync().ConfigureAwait(false)
+                : 0;
+
+            return new User(
+                name,
+                _defaultAuthenticationProvider.GetType().FullName,
+                _defaultPasswordResetProvider.GetType().FullName)
+            {
+                InternalId = max + 1
+            };
+        }
+
         /// <inheritdoc/>
-        public User CreateUser(string name)
+        public async Task<User> CreateUserAsync(string name)
         {
             if (!IsValidUsername(name))
             {
@@ -198,18 +229,10 @@ namespace Jellyfin.Server.Implementations.Users
 
             using var dbContext = _dbProvider.CreateContext();
 
-            // TODO: Remove after user item data is migrated.
-            var max = dbContext.Users.Any() ? dbContext.Users.Select(u => u.InternalId).Max() : 0;
+            var newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
 
-            var newUser = new User(
-                name,
-                _defaultAuthenticationProvider.GetType().FullName,
-                _defaultPasswordResetProvider.GetType().FullName)
-            {
-                InternalId = max + 1
-            };
             dbContext.Users.Add(newUser);
-            dbContext.SaveChanges();
+            await dbContext.SaveChangesAsync().ConfigureAwait(false);
 
             OnUserCreated?.Invoke(this, new GenericEventArgs<User>(newUser));
 
@@ -512,7 +535,7 @@ namespace Jellyfin.Server.Implementations.Users
             }
             else
             {
-                IncrementInvalidLoginAttemptCount(user);
+                await IncrementInvalidLoginAttemptCount(user).ConfigureAwait(false);
                 _logger.LogInformation(
                     "Authentication request for {UserName} has been denied (IP: {IP}).",
                     user.Username,
@@ -530,7 +553,12 @@ namespace Jellyfin.Server.Implementations.Users
             if (user != null && isInNetwork)
             {
                 var passwordResetProvider = GetPasswordResetProvider(user);
-                return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false);
+                var result = await passwordResetProvider
+                    .StartForgotPasswordProcess(user, isInNetwork)
+                    .ConfigureAwait(false);
+
+                await UpdateUserAsync(user).ConfigureAwait(false);
+                return result;
             }
 
             return new ForgotPasswordResult
@@ -560,24 +588,13 @@ namespace Jellyfin.Server.Implementations.Users
             };
         }
 
-        /// <inheritdoc/>
-        public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders)
-        {
-            _authenticationProviders = authenticationProviders.ToArray();
-            _passwordResetProviders = passwordResetProviders.ToArray();
-
-            _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
-            _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
-            _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
-        }
-
         /// <inheritdoc />
-        public void Initialize()
+        public async Task InitializeAsync()
         {
             // TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
             using var dbContext = _dbProvider.CreateContext();
 
-            if (dbContext.Users.Any())
+            if (await dbContext.Users.AnyAsync().ConfigureAwait(false))
             {
                 return;
             }
@@ -595,13 +612,13 @@ namespace Jellyfin.Server.Implementations.Users
                 throw new ArgumentException("Provided username is not valid!", defaultName);
             }
 
-            var newUser = CreateUser(defaultName);
+            var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
             newUser.SetPermission(PermissionKind.IsAdministrator, true);
             newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
             newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
 
-            dbContext.Users.Update(newUser);
-            dbContext.SaveChanges();
+            dbContext.Users.Add(newUser);
+            await dbContext.SaveChangesAsync().ConfigureAwait(false);
         }
 
         /// <inheritdoc/>
@@ -637,7 +654,7 @@ namespace Jellyfin.Server.Implementations.Users
         /// <inheritdoc/>
         public void UpdateConfiguration(Guid userId, UserConfiguration config)
         {
-            var dbContext = _dbProvider.CreateContext();
+            using var dbContext = _dbProvider.CreateContext();
             var user = dbContext.Users
                            .Include(u => u.Permissions)
                            .Include(u => u.Preferences)
@@ -670,8 +687,14 @@ namespace Jellyfin.Server.Implementations.Users
         /// <inheritdoc/>
         public void UpdatePolicy(Guid userId, UserPolicy policy)
         {
-            var dbContext = _dbProvider.CreateContext();
-            var user = dbContext.Users.Find(userId) ?? throw new ArgumentException("No user exists with given Id!");
+            using var dbContext = _dbProvider.CreateContext();
+            var user = dbContext.Users
+                           .Include(u => u.Permissions)
+                           .Include(u => u.Preferences)
+                           .Include(u => u.AccessSchedules)
+                           .Include(u => u.ProfileImage)
+                           .FirstOrDefault(u => u.Id == userId)
+                       ?? throw new ArgumentException("No user exists with given Id!");
 
             // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
             int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
@@ -876,7 +899,7 @@ namespace Jellyfin.Server.Implementations.Users
             }
         }
 
-        private void IncrementInvalidLoginAttemptCount(User user)
+        private async Task IncrementInvalidLoginAttemptCount(User user)
         {
             user.InvalidLoginAttemptCount++;
             int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
@@ -890,7 +913,7 @@ namespace Jellyfin.Server.Implementations.Users
                     user.InvalidLoginAttemptCount);
             }
 
-            UpdateUser(user);
+            await UpdateUserAsync(user).ConfigureAwait(false);
         }
     }
 }

+ 2 - 2
Jellyfin.Server/CoreAppHost.cs

@@ -34,9 +34,9 @@ namespace Jellyfin.Server
         /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param>
         public CoreAppHost(
-            ServerApplicationPaths applicationPaths,
+            IServerApplicationPaths applicationPaths,
             ILoggerFactory loggerFactory,
-            StartupOptions options,
+            IStartupOptions options,
             IFileSystem fileSystem,
             INetworkManager networkManager)
             : base(

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

@@ -41,8 +41,8 @@
 
   <ItemGroup>
     <PackageReference Include="CommandLineParser" Version="2.8.0" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.5" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.5" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.6" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.6" />
     <PackageReference Include="prometheus-net" Version="3.6.0" />
     <PackageReference Include="prometheus-net.AspNetCore" Version="3.6.0" />
     <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />

+ 5 - 0
Jellyfin.Server/Migrations/IMigrationRoutine.cs

@@ -17,6 +17,11 @@ namespace Jellyfin.Server.Migrations
         /// </summary>
         public string Name { get; }
 
+        /// <summary>
+        /// Gets a value indicating whether to perform migration on a new install.
+        /// </summary>
+        public bool PerformOnNewInstall { get; }
+
         /// <summary>
         /// Execute the migration routine.
         /// </summary>

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

@@ -22,6 +22,7 @@ namespace Jellyfin.Server.Migrations
             typeof(Routines.RemoveDuplicateExtras),
             typeof(Routines.AddDefaultPluginRepository),
             typeof(Routines.MigrateUserDb),
+            typeof(Routines.ReaddDefaultPluginRepository),
             typeof(Routines.MigrateDisplayPreferencesDb)
         };
 
@@ -44,9 +45,8 @@ namespace Jellyfin.Server.Migrations
                 // If startup wizard is not finished, this is a fresh install.
                 // Don't run any migrations, just mark all of them as applied.
                 logger.LogInformation("Marking all known migrations as applied because this is a fresh install");
-                migrationOptions.Applied.AddRange(migrations.Select(m => (m.Id, m.Name)));
+                migrationOptions.Applied.AddRange(migrations.Where(m => !m.PerformOnNewInstall).Select(m => (m.Id, m.Name)));
                 host.ServerConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions);
-                return;
             }
 
             var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet();

+ 3 - 0
Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs

@@ -32,6 +32,9 @@ namespace Jellyfin.Server.Migrations.Routines
         /// <inheritdoc/>
         public string Name => "AddDefaultPluginRepository";
 
+        /// <inheritdoc/>
+        public bool PerformOnNewInstall => true;
+
         /// <inheritdoc/>
         public void Perform()
         {

+ 3 - 0
Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs

@@ -48,6 +48,9 @@ namespace Jellyfin.Server.Migrations.Routines
         /// <inheritdoc/>
         public string Name => "CreateLoggingConfigHeirarchy";
 
+        /// <inheritdoc/>
+        public bool PerformOnNewInstall => false;
+
         /// <inheritdoc/>
         public void Perform()
         {

+ 3 - 0
Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs

@@ -25,6 +25,9 @@ namespace Jellyfin.Server.Migrations.Routines
         /// <inheritdoc/>
         public string Name => "DisableTranscodingThrottling";
 
+        /// <inheritdoc/>
+        public bool PerformOnNewInstall => false;
+
         /// <inheritdoc/>
         public void Perform()
         {

+ 3 - 0
Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs

@@ -41,6 +41,9 @@ namespace Jellyfin.Server.Migrations.Routines
         /// <inheritdoc/>
         public string Name => "MigrateActivityLogDatabase";
 
+        /// <inheritdoc/>
+        public bool PerformOnNewInstall => false;
+
         /// <inheritdoc/>
         public void Perform()
         {

+ 3 - 0
Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs

@@ -54,6 +54,9 @@ namespace Jellyfin.Server.Migrations.Routines
         /// <inheritdoc/>
         public string Name => "MigrateUserDatabase";
 
+        /// <inheritdoc/>
+        public bool PerformOnNewInstall => false;
+
         /// <inheritdoc/>
         public void Perform()
         {

+ 49 - 0
Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs

@@ -0,0 +1,49 @@
+using System;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.Updates;
+
+namespace Jellyfin.Server.Migrations.Routines
+{
+    /// <summary>
+    /// Migration to initialize system configuration with the default plugin repository.
+    /// </summary>
+    public class ReaddDefaultPluginRepository : IMigrationRoutine
+    {
+        private readonly IServerConfigurationManager _serverConfigurationManager;
+
+        private readonly RepositoryInfo _defaultRepositoryInfo = new RepositoryInfo
+        {
+            Name = "Jellyfin Stable",
+            Url = "https://repo.jellyfin.org/releases/plugin/manifest-stable.json"
+        };
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ReaddDefaultPluginRepository"/> class.
+        /// </summary>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        public ReaddDefaultPluginRepository(IServerConfigurationManager serverConfigurationManager)
+        {
+            _serverConfigurationManager = serverConfigurationManager;
+        }
+
+        /// <inheritdoc/>
+        public Guid Id => Guid.Parse("5F86E7F6-D966-4C77-849D-7A7B40B68C4E");
+
+        /// <inheritdoc/>
+        public string Name => "ReaddDefaultPluginRepository";
+
+        /// <inheritdoc/>
+        public bool PerformOnNewInstall => true;
+
+        /// <inheritdoc/>
+        public void Perform()
+        {
+            // Only add if repository list is empty
+            if (_serverConfigurationManager.Configuration.PluginRepositories.Count == 0)
+            {
+                _serverConfigurationManager.Configuration.PluginRepositories.Add(_defaultRepositoryInfo);
+                _serverConfigurationManager.SaveConfiguration();
+            }
+        }
+    }
+}

+ 3 - 0
Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs

@@ -29,6 +29,9 @@ namespace Jellyfin.Server.Migrations.Routines
         /// <inheritdoc/>
         public string Name => "RemoveDuplicateExtras";
 
+        /// <inheritdoc/>
+        public bool PerformOnNewInstall => false;
+
         /// <inheritdoc/>
         public void Perform()
         {

+ 15 - 0
Jellyfin.Server/Program.cs

@@ -343,6 +343,21 @@ namespace Jellyfin.Server
                             }
                         }
                     }
+
+                    // Bind to unix socket (only on OSX and Linux)
+                    if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+                    {
+                        // TODO: allow configuration of socket path
+                        var socketPath = $"{appPaths.DataPath}/socket.sock";
+                        // Workaround for https://github.com/aspnet/AspNetCore/issues/14134
+                        if (File.Exists(socketPath))
+                        {
+                            File.Delete(socketPath);
+                        }
+
+                        options.ListenUnixSocket(socketPath);
+                        _logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath);
+                    }
                 })
                 .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(commandLineOpts, appPaths, startupConfig))
                 .UseSerilog()

+ 5 - 0
MediaBrowser.Api/Images/ImageService.cs

@@ -895,6 +895,11 @@ namespace MediaBrowser.Api.Images
             // Handle image/png; charset=utf-8
             mimeType = mimeType.Split(';').FirstOrDefault();
             var userDataPath = Path.Combine(ServerConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
+            if (user.ProfileImage != null)
+            {
+                _userManager.ClearProfileImage(user);
+            }
+            
             user.ProfileImage = new Jellyfin.Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
 
             await _providerManager

+ 4 - 1
MediaBrowser.Api/System/ActivityLogService.cs

@@ -56,7 +56,10 @@ namespace MediaBrowser.Api.System
                 DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
 
             var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>(
-                entries => entries.Where(entry => entry.DateCreated >= minDate));
+                entries => entries.Where(entry => entry.DateCreated >= minDate
+                                                  && (!request.HasUserId.HasValue || (request.HasUserId.Value
+                                                      ? entry.UserId != Guid.Empty
+                                                      : entry.UserId == Guid.Empty))));
 
             var result = _activityManager.GetPagedResult(filterFunc, request.StartIndex, request.Limit);
 

+ 1 - 1
MediaBrowser.Api/UserService.cs

@@ -525,7 +525,7 @@ namespace MediaBrowser.Api
         /// <returns>System.Object.</returns>
         public async Task<object> Post(CreateUserByName request)
         {
-            var newUser = _userManager.CreateUser(request.Name);
+            var newUser = await _userManager.CreateUserAsync(request.Name).ConfigureAwait(false);
 
             // no need to authenticate password for new user
             if (request.Password != null)

+ 33 - 0
MediaBrowser.Common/Extensions/HttpContextExtensions.cs

@@ -0,0 +1,33 @@
+using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
+
+namespace MediaBrowser.Common.Extensions
+{
+    /// <summary>
+    /// Static class containing extension methods for <see cref="HttpContext"/>.
+    /// </summary>
+    public static class HttpContextExtensions
+    {
+        private const string ServiceStackRequest = "ServiceStackRequest";
+
+        /// <summary>
+        /// Set the ServiceStack request.
+        /// </summary>
+        /// <param name="httpContext">The HttpContext instance.</param>
+        /// <param name="request">The service stack request instance.</param>
+        public static void SetServiceStackRequest(this HttpContext httpContext, IRequest request)
+        {
+            httpContext.Items[ServiceStackRequest] = request;
+        }
+
+        /// <summary>
+        /// Get the ServiceStack request.
+        /// </summary>
+        /// <param name="httpContext">The HttpContext instance.</param>
+        /// <returns>The service stack request instance.</returns>
+        public static IRequest GetServiceStackRequest(this HttpContext httpContext)
+        {
+            return (IRequest)httpContext.Items[ServiceStackRequest];
+        }
+    }
+}

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

@@ -17,8 +17,8 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.5" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.6" />
     <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
   </ItemGroup>
 

+ 1 - 1
MediaBrowser.Controller/Library/ILibraryManager.cs

@@ -200,7 +200,7 @@ namespace MediaBrowser.Controller.Library
         /// <summary>
         /// Updates the item.
         /// </summary>
-        void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
+        void UpdateItems(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
 
         void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
 

+ 2 - 5
MediaBrowser.Controller/Library/IUserManager.cs

@@ -2,7 +2,6 @@ using System;
 using System.Collections.Generic;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Events;
@@ -55,7 +54,7 @@ namespace MediaBrowser.Controller.Library
         /// <summary>
         /// Initializes the user manager and ensures that a user exists.
         /// </summary>
-        void Initialize();
+        Task InitializeAsync();
 
         /// <summary>
         /// Gets a user by Id.
@@ -106,7 +105,7 @@ namespace MediaBrowser.Controller.Library
         /// <returns>The created user.</returns>
         /// <exception cref="ArgumentNullException">name</exception>
         /// <exception cref="ArgumentException"></exception>
-        User CreateUser(string name);
+        Task<User> CreateUserAsync(string name);
 
         /// <summary>
         /// Deletes the specified user.
@@ -166,8 +165,6 @@ namespace MediaBrowser.Controller.Library
         /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
         Task<PinRedeemResult> RedeemPasswordResetPin(string pin);
 
-        void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders);
-
         NameIdPair[] GetAuthenticationProviders();
 
         NameIdPair[] GetPasswordResetProviders();

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

@@ -13,8 +13,8 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.5" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.6" />
   </ItemGroup>
 
   <ItemGroup>

+ 249 - 146
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Linq;
+using System.Runtime.InteropServices;
 using System.Text;
 using System.Threading;
 using Jellyfin.Data.Enums;
@@ -371,7 +372,7 @@ namespace MediaBrowser.Controller.MediaEncoding
         public int GetVideoProfileScore(string profile)
         {
             // strip spaces because they may be stripped out on the query string
-            profile = profile.Replace(" ", "");
+            profile = profile.Replace(" ", string.Empty, StringComparison.Ordinal);
             return Array.FindIndex(_videoProfiles, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase));
         }
 
@@ -449,41 +450,59 @@ namespace MediaBrowser.Controller.MediaEncoding
             var arg = new StringBuilder();
             var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty;
             var outputVideoCodec = GetVideoEncoder(state, encodingOptions) ?? string.Empty;
-            bool isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
-            bool isVaapiEncoder = outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
-            bool isQsvDecoder = videoDecoder.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
-            bool isQsvEncoder = outputVideoCodec.IndexOf("qsv", 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 isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+            var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
 
-            if (state.IsVideoRequest
-                && string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
+            if (!IsCopyCodec(outputVideoCodec))
             {
-                if (isVaapiDecoder)
+                if (state.IsVideoRequest
+                    && _mediaEncoder.SupportsHwaccel("vaapi")
+                    && string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
                 {
-                    arg.Append("-hwaccel_output_format vaapi ")
-                        .Append("-vaapi_device ")
-                        .Append(encodingOptions.VaapiDevice)
-                        .Append(" ");
-                }
-                else if (!isVaapiDecoder && isVaapiEncoder)
-                {
-                    arg.Append("-vaapi_device ")
-                        .Append(encodingOptions.VaapiDevice)
-                        .Append(" ");
+                    if (isVaapiDecoder)
+                    {
+                        arg.Append("-hwaccel_output_format vaapi ")
+                            .Append("-vaapi_device ")
+                            .Append(encodingOptions.VaapiDevice)
+                            .Append(' ');
+                    }
+                    else if (!isVaapiDecoder && isVaapiEncoder)
+                    {
+                        arg.Append("-vaapi_device ")
+                            .Append(encodingOptions.VaapiDevice)
+                            .Append(' ');
+                    }
                 }
-            }
-
-            if (state.IsVideoRequest
-                && string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
-            {
-                var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
 
-                if (!hasTextSubs)
+                if (state.IsVideoRequest
+                    && string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
                 {
-                     if (isQsvEncoder)
+                    var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+
+                    if (isQsvEncoder)
                     {
                         if (isQsvDecoder)
                         {
-                            arg.Append("-hwaccel qsv -init_hw_device qsv=hw ");
+                            if (isLinux)
+                            {
+                                if (hasGraphicalSubs)
+                                {
+                                    arg.Append("-init_hw_device qsv=hw -filter_hw_device hw ");
+                                }
+                                else
+                                {
+                                    arg.Append("-hwaccel qsv ");
+                                }
+                            }
+
+                            if (isWindows)
+                            {
+                                arg.Append("-hwaccel qsv ");
+                            }
                         }
                         // While using SW decoder
                         else
@@ -806,6 +825,34 @@ namespace MediaBrowser.Controller.MediaEncoding
                         break;
                 }
             }
+            else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
+                || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+            {
+                switch (encodingOptions.EncoderPreset)
+                {
+                    case "veryslow":
+                    case "slow":
+                    case "slower":
+                        param += "-quality quality";
+                        break;
+
+                    case "medium":
+                        param += "-quality balanced";
+                        break;
+
+                    case "fast":
+                    case "faster":
+                    case "veryfast":
+                    case "superfast":
+                    case "ultrafast":
+                        param += "-quality speed";
+                        break;
+
+                    default:
+                        param += "-quality speed";
+                        break;
+                }
+            }
             else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // webm
             {
                 // Values 0-3, 0 being highest quality but slower
@@ -1351,7 +1398,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 transcoderChannelLimit = 6;
             }
 
-            var isTranscodingAudio = !EncodingHelper.IsCopyCodec(codec);
+            var isTranscodingAudio = !IsCopyCodec(codec);
 
             int? resultChannels = state.GetRequestedAudioChannels(codec);
             if (isTranscodingAudio)
@@ -1555,28 +1602,44 @@ namespace MediaBrowser.Controller.MediaEncoding
                 var index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase);
                 if (index != -1)
                 {
-                    outputSizeParam = "," + outputSizeParam.Substring(index);
+                    outputSizeParam = outputSizeParam.Substring(index);
                 }
                 else
                 {
-                    index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase);
+                    index = outputSizeParam.IndexOf("hwupload=extra_hw_frames", StringComparison.OrdinalIgnoreCase);
                     if (index != -1)
                     {
-                        outputSizeParam = "," + outputSizeParam.Substring(index);
+                        outputSizeParam = outputSizeParam.Substring(index);
                     }
                     else
                     {
-                        index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase);
+                        index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase);
                         if (index != -1)
                         {
-                            outputSizeParam = "," + outputSizeParam.Substring(index);
+                            outputSizeParam = outputSizeParam.Substring(index);
                         }
                         else
                         {
-                            index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase);
+                            index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase);
                             if (index != -1)
                             {
-                                outputSizeParam = "," + outputSizeParam.Substring(index);
+                                outputSizeParam = outputSizeParam.Substring(index);
+                            }
+                            else
+                            {
+                                index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase);
+                                if (index != -1)
+                                {
+                                    outputSizeParam = outputSizeParam.Substring(index);
+                                }
+                                else
+                                {
+                                    index = outputSizeParam.IndexOf("vpp", StringComparison.OrdinalIgnoreCase);
+                                    if (index != -1)
+                                    {
+                                        outputSizeParam = outputSizeParam.Substring(index);
+                                    }
+                                }
                             }
                         }
                     }
@@ -1585,43 +1648,30 @@ namespace MediaBrowser.Controller.MediaEncoding
 
             var videoSizeParam = string.Empty;
             var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options) ?? string.Empty;
+            var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
 
             // Setup subtitle scaling
             if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue)
             {
-                videoSizeParam = string.Format(
-                    CultureInfo.InvariantCulture,
-                    "scale={0}:{1}",
-                    state.VideoStream.Width.Value,
-                    state.VideoStream.Height.Value);
-
-                // For QSV, feed it into hardware encoder now
-                if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
-                {
-                    videoSizeParam += ",hwupload=extra_hw_frames=64";
-                }
+                // Adjust the size of graphical subtitles to fit the video stream.
+                var videoStream = state.VideoStream;
+                var inputWidth = videoStream?.Width;
+                var inputHeight = videoStream?.Height;
+                var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight);
 
-                // For VAAPI and CUVID decoder
-                // these encoders cannot automatically adjust the size of graphical subtitles to fit the output video,
-                // thus needs to be manually adjusted.
-                if (videoDecoder.IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1
-                    || (IsVaapiSupported(state) && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)
-                        && (videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
-                            || outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1)))
+                if (width.HasValue && height.HasValue)
                 {
-                    var videoStream = state.VideoStream;
-                    var inputWidth = videoStream?.Width;
-                    var inputHeight = videoStream?.Height;
-                    var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight);
-
-                    if (width.HasValue && height.HasValue)
-                    {
-                        videoSizeParam = string.Format(
+                    videoSizeParam = string.Format(
                         CultureInfo.InvariantCulture,
-                        "scale={0}:{1}",
+                        "scale={0}x{1}",
                         width.Value,
                         height.Value);
-                    }
+                }
+
+                // For QSV, feed it into hardware encoder now
+                if (isLinux && string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+                {
+                    videoSizeParam += ",hwupload=extra_hw_frames=64";
                 }
             }
 
@@ -1634,7 +1684,10 @@ namespace MediaBrowser.Controller.MediaEncoding
                 : state.SubtitleStream.Index;
 
             // Setup default filtergraph utilizing FFMpeg overlay() and FFMpeg scale() (see the return of this function for index reference)
-            var retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay{3}\"";
+            // Always put the scaler before the overlay for better performance
+            var retStr = !string.IsNullOrEmpty(outputSizeParam) ?
+                " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"" :
+                " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\"";
 
             // When the input may or may not be hardware VAAPI decodable
             if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
@@ -1644,12 +1697,11 @@ namespace MediaBrowser.Controller.MediaEncoding
                     [sub]: SW scaling subtitle to FixedOutputSize
                     [base][sub]: SW overlay
                 */
-                outputSizeParam = outputSizeParam.TrimStart(',');
                 retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3},hwdownload[base];[base][sub]overlay,format=nv12,hwupload\"";
             }
 
             // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
-            else if (IsVaapiSupported(state) && videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
+            else if (_mediaEncoder.SupportsHwaccel("vaapi") && videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
                 && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
             {
                 /*
@@ -1657,7 +1709,6 @@ namespace MediaBrowser.Controller.MediaEncoding
                     [sub]: SW scaling subtitle to FixedOutputSize
                     [base][sub]: SW overlay
                 */
-                outputSizeParam = outputSizeParam.TrimStart(',');
                 retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"";
             }
             else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
@@ -1666,14 +1717,13 @@ namespace MediaBrowser.Controller.MediaEncoding
                     QSV in FFMpeg can now setup hardware overlay for transcodes.
                     For software decoding and hardware encoding option, frames must be hwuploaded into hardware
                     with fixed frame size.
+                    Currently only supports linux.
                 */
-                if (videoDecoder.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1)
-                {
-                    retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv=x=(W-w)/2:y=(H-h)/2{3}\"";
-                }
-                else
+                if (isLinux)
                 {
-                    retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]hwupload=extra_hw_frames=64[v];[v][sub]overlay_qsv=x=(W-w)/2:y=(H-h)/2{3}\"";
+                    retStr = !string.IsNullOrEmpty(outputSizeParam) ?
+                        " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_qsv\"" :
+                        " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv\"";
                 }
             }
 
@@ -1745,10 +1795,8 @@ namespace MediaBrowser.Controller.MediaEncoding
                 requestedMaxWidth,
                 requestedMaxHeight);
 
-            var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
-
             if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
-                || (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) && !hasTextSubs))
+                || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
                 && width.HasValue
                 && height.HasValue)
             {
@@ -1758,6 +1806,10 @@ namespace MediaBrowser.Controller.MediaEncoding
                 var outputWidth = width.Value;
                 var outputHeight = height.Value;
                 var qsv_or_vaapi = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase);
+                var isDeintEnabled = state.DeInterlace("h264", true)
+                    || state.DeInterlace("avc", true)
+                    || state.DeInterlace("h265", true)
+                    || state.DeInterlace("hevc", true);
 
                 if (!videoWidth.HasValue
                     || outputWidth != videoWidth.Value
@@ -1769,15 +1821,20 @@ namespace MediaBrowser.Controller.MediaEncoding
                     filters.Add(
                         string.Format(
                             CultureInfo.InvariantCulture,
-                            "{0}=w={1}:h={2}:format=nv12",
+                            "{0}=w={1}:h={2}:format=nv12{3}",
                             qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi",
                             outputWidth,
-                            outputHeight));
+                            outputHeight,
+                            (qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty));
                 }
                 else
                 {
-                    // set w=0:h=0 for vpp_qsv to keep the original dimensions, otherwise it will fail.
-                    filters.Add(string.Format(CultureInfo.InvariantCulture, "{0}format=nv12", qsv_or_vaapi ? "vpp_qsv=w=0:h=0:" : "scale_vaapi="));
+                    filters.Add(
+                        string.Format(
+                            CultureInfo.InvariantCulture,
+                            "{0}=format=nv12{1}",
+                            qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi",
+                            (qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty));
                 }
             }
             else if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1
@@ -1989,7 +2046,6 @@ namespace MediaBrowser.Controller.MediaEncoding
             // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/
 
             var request = state.BaseRequest;
-
             var videoStream = state.VideoStream;
             var filters = new List<string>();
 
@@ -1998,32 +2054,34 @@ namespace MediaBrowser.Controller.MediaEncoding
             var inputHeight = videoStream?.Height;
             var threeDFormat = state.MediaSource.Video3DFormat;
 
+            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;
+            var isNvdecH264Decoder = videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
+            var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1;
+            var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+
             var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+            var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
 
             // When the input may or may not be hardware VAAPI decodable
-            if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+            if (isVaapiH264Encoder)
             {
                 filters.Add("format=nv12|vaapi");
                 filters.Add("hwupload");
             }
 
-            // When the input may or may not be hardware QSV decodable
-            else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+            // When burning in graphical subtitles using overlay_qsv, upload videostream to the same qsv context
+            else if (isLinux && hasGraphicalSubs && isQsvH264Encoder)
             {
-                if (!hasTextSubs)
-                {
-                    filters.Add("format=nv12|qsv");
-                    filters.Add("hwupload=extra_hw_frames=64");
-                }
+                filters.Add("hwupload=extra_hw_frames=64");
             }
 
             // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
-            else if (videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
-                && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
+            else if (IsVaapiSupported(state) && isVaapiDecoder && isLibX264Encoder)
             {
                 var codec = videoStream.Codec.ToLowerInvariant();
-                var isColorDepth10 = !string.IsNullOrEmpty(videoStream.Profile) && (videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase)
-                    || videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase));
+                var isColorDepth10 = IsColorDepth10(state);
 
                 // Assert 10-bit hardware VAAPI decodable
                 if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
@@ -2048,49 +2106,49 @@ namespace MediaBrowser.Controller.MediaEncoding
             }
 
             // Add hardware deinterlace filter before scaling filter
-            if (state.DeInterlace("h264", true))
+            if (state.DeInterlace("h264", true) || state.DeInterlace("avc", true))
             {
-                if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+                if (isVaapiH264Encoder)
                 {
                     filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_vaapi"));
                 }
-                else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
-                {
-                    if (!hasTextSubs)
-                    {
-                        filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_qsv"));
-                    }
-                }
             }
 
             // Add software deinterlace filter before scaling filter
-            if (((state.DeInterlace("h264", true) || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true))
-                && !string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
-                && !string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
-                    || (hasTextSubs && state.DeInterlace("h264", true) && string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)))
+            if (state.DeInterlace("h264", true)
+                || state.DeInterlace("avc", true)
+                || state.DeInterlace("h265", true)
+                || state.DeInterlace("hevc", true))
             {
+                var deintParam = string.Empty;
                 var inputFramerate = videoStream?.RealFrameRate;
 
                 // If it is already 60fps then it will create an output framerate that is much too high for roku and others to handle
                 if (string.Equals(options.DeinterlaceMethod, "yadif_bob", StringComparison.OrdinalIgnoreCase) && (inputFramerate ?? 60) <= 30)
                 {
-                    filters.Add("yadif=1:-1:0");
+                    deintParam = "yadif=1:-1:0";
                 }
                 else
                 {
-                    filters.Add("yadif=0:-1:0");
+                    deintParam = "yadif=0:-1:0";
+                }
+
+                if (!string.IsNullOrEmpty(deintParam))
+                {
+                    if (!isVaapiH264Encoder && !isQsvH264Encoder && !isNvdecH264Decoder)
+                    {
+                        filters.Add(deintParam);
+                    }
                 }
             }
 
             // 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));
 
-            // Add parameters to use VAAPI with burn-in text subttiles (GH issue #642)
-            if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+            // Add parameters to use VAAPI with burn-in text subtitles (GH issue #642)
+            if (isVaapiH264Encoder)
             {
-                if (state.SubtitleStream != null
-                    && state.SubtitleStream.IsTextSubtitleStream
-                    && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+                if (hasTextSubs)
                 {
                     // Test passed on Intel and AMD gfx
                     filters.Add("hwmap=mode=read+write");
@@ -2100,9 +2158,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 
             var output = string.Empty;
 
-            if (state.SubtitleStream != null
-                && state.SubtitleStream.IsTextSubtitleStream
-                && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+            if (hasTextSubs)
             {
                 var subParam = GetTextSubtitleParam(state);
 
@@ -2110,7 +2166,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 
                 // Ensure proper filters are passed to ffmpeg in case of hardware acceleration via VA-API
                 // Reference: https://trac.ffmpeg.org/wiki/Hardware/VAAPI
-                if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+                if (isVaapiH264Encoder)
                 {
                     filters.Add("hwmap");
                 }
@@ -2264,7 +2320,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 flags.Add("+ignidx");
             }
 
-            if (state.GenPtsInput || EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+            if (state.GenPtsInput || IsCopyCodec(state.OutputVideoCodec))
             {
                 flags.Add("+genpts");
             }
@@ -2290,7 +2346,8 @@ namespace MediaBrowser.Controller.MediaEncoding
             {
                 inputModifier += " " + videoDecoder;
 
-                if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1)
+                if (!IsCopyCodec(state.OutputVideoCodec)
+                    && (videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1)
                 {
                     var videoStream = state.VideoStream;
                     var inputWidth = videoStream?.Width;
@@ -2303,11 +2360,19 @@ namespace MediaBrowser.Controller.MediaEncoding
                         && width.HasValue
                         && height.HasValue)
                     {
-                        inputModifier += string.Format(
-                            CultureInfo.InvariantCulture,
-                            " -resize {0}x{1}",
-                            width.Value,
-                            height.Value);
+                        if (width.HasValue && height.HasValue)
+                        {
+                            inputModifier += string.Format(
+                                CultureInfo.InvariantCulture,
+                                " -resize {0}x{1}",
+                                width.Value,
+                                height.Value);
+                        }
+
+                        if (state.DeInterlace("h264", true))
+                        {
+                            inputModifier += " -deint 1";
+                        }
                     }
                 }
             }
@@ -2523,21 +2588,21 @@ namespace MediaBrowser.Controller.MediaEncoding
         }
 
         /// <summary>
-        /// Gets the name of the output video codec.
+        /// Gets the ffmpeg option string for the hardware accelerated video decoder.
         /// </summary>
+        /// <param name="state">The encoding job info.</param>
+        /// <param name="encodingOptions">The encoding options.</param>
+        /// <returns>The option string or null if none available.</returns>
         protected string GetHardwareAcceleratedVideoDecoder(EncodingJobInfo state, EncodingOptions encodingOptions)
         {
-            var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile;
             var videoStream = state.VideoStream;
-            var isColorDepth10 = !string.IsNullOrEmpty(videoStream.Profile) && (videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase)
-                || videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase));
-
 
-            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+            if (videoStream == null)
             {
                 return null;
             }
 
+            var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile;
             // Only use alternative encoders for video files.
             // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
             // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
@@ -2546,10 +2611,15 @@ namespace MediaBrowser.Controller.MediaEncoding
                 return null;
             }
 
-            if (videoStream != null
-                && !string.IsNullOrEmpty(videoStream.Codec)
-                && !string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
+            if (IsCopyCodec(state.OutputVideoCodec))
             {
+                return null;
+            }
+
+            if (!string.IsNullOrEmpty(videoStream.Codec) && !string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
+            {
+                var isColorDepth10 = IsColorDepth10(state);
+
                 // Only hevc and vp9 formats have 10-bit hardware decoder support now.
                 if (isColorDepth10 && !(string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)
                     || string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase)
@@ -2625,13 +2695,6 @@ namespace MediaBrowser.Controller.MediaEncoding
                         case "h264":
                             if (_mediaEncoder.SupportsDecoder("h264_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase))
                             {
-                                // cuvid decoder does not support 10-bit input.
-                                if ((videoStream.BitDepth ?? 8) > 8)
-                                {
-                                    encodingOptions.HardwareDecodingCodecs = Array.Empty<string>();
-                                    return null;
-                                }
-
                                 return "-c:v h264_cuvid";
                             }
 
@@ -2898,21 +2961,24 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// </summary>
         public string GetHwaccelType(EncodingJobInfo state, EncodingOptions options, string videoCodec)
         {
-            var isWindows = Environment.OSVersion.Platform == PlatformID.Win32NT;
+            var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+            var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
             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 (!isWindows)
+                if (isLinux)
                 {
                     return "-hwaccel vaapi";
                 }
-                else if (isWindows8orLater)
+
+                if (isWindows && isWindows8orLater)
                 {
                     return "-hwaccel d3d11va";
                 }
-                else
+
+                if (isWindows && !isWindows8orLater)
                 {
                     return "-hwaccel dxva2";
                 }
@@ -3002,7 +3068,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 args += " -mpegts_m2ts_mode 1";
             }
 
-            if (EncodingHelper.IsCopyCodec(videoCodec))
+            if (IsCopyCodec(videoCodec))
             {
                 if (state.VideoStream != null
                     && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase)
@@ -3104,7 +3170,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 
             var args = "-codec:a:0 " + codec;
 
-            if (EncodingHelper.IsCopyCodec(codec))
+            if (IsCopyCodec(codec))
             {
                 return args;
             }
@@ -3181,5 +3247,42 @@ namespace MediaBrowser.Controller.MediaEncoding
         {
             return string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase);
         }
+
+        public static bool IsColorDepth10(EncodingJobInfo state)
+        {
+            var result = false;
+            var videoStream = state.VideoStream;
+
+            if (videoStream != null)
+            {
+                if (!string.IsNullOrEmpty(videoStream.PixelFormat))
+                {
+                    result = videoStream.PixelFormat.Contains("p10", StringComparison.OrdinalIgnoreCase);
+                    if (result)
+                    {
+                        return true;
+                    }
+                }
+
+                if (!string.IsNullOrEmpty(videoStream.Profile))
+                {
+                    result = videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase)
+                        || videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase)
+                        || videoStream.Profile.Contains("Profile 2", StringComparison.OrdinalIgnoreCase);
+                    if (result)
+                    {
+                        return true;
+                    }
+                }
+
+                result = (videoStream.BitDepth ?? 8) == 10;
+                if (result)
+                {
+                    return true;
+                }
+            }
+
+            return result;
+        }
     }
 }

+ 2 - 2
MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs

@@ -198,7 +198,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
         internal static Version GetFFmpegVersion(string output)
         {
             // For pre-built binaries the FFmpeg version should be mentioned at the very start of the output
-            var match = Regex.Match(output, @"^ffmpeg version n?((?:\d+\.?)+)");
+            var match = Regex.Match(output, @"^ffmpeg version n?((?:[0-9]+\.?)+)");
 
             if (match.Success)
             {
@@ -225,7 +225,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             var rc = new StringBuilder(144);
             foreach (Match m in Regex.Matches(
                 output,
-                @"((?<name>lib\w+)\s+(?<major>\d+)\.\s*(?<minor>\d+))",
+                @"((?<name>lib\w+)\s+(?<major>[0-9]+)\.\s*(?<minor>[0-9]+))",
                 RegexOptions.Multiline))
             {
                 rc.Append(m.Groups["name"])

+ 1 - 1
MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs

@@ -172,7 +172,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                 inputFiles = new[] { mediaSource.Path };
             }
 
-            var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, mediaSource.Protocol, subtitleStream, cancellationToken).ConfigureAwait(false);
+            var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), subtitleStream, cancellationToken).ConfigureAwait(false);
 
             var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false);
 

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

@@ -23,7 +23,7 @@
 
   <ItemGroup>
     <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
-    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.5" />
+    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.6" />
     <PackageReference Include="System.Globalization" Version="4.3.0" />
     <PackageReference Include="System.Text.Json" Version="4.7.2" />
   </ItemGroup>

+ 10 - 3
MediaBrowser.Model/Notifications/NotificationOption.cs

@@ -1,3 +1,4 @@
+#pragma warning disable CA1819 // Properties should not return arrays
 #pragma warning disable CS1591
 
 using System;
@@ -9,21 +10,27 @@ namespace MediaBrowser.Model.Notifications
         public NotificationOption(string type)
         {
             Type = type;
+            DisabledServices = Array.Empty<string>();
+            DisabledMonitorUsers = Array.Empty<string>();
+            SendToUsers = Array.Empty<string>();
+        }
 
+        public NotificationOption()
+        {
             DisabledServices = Array.Empty<string>();
             DisabledMonitorUsers = Array.Empty<string>();
             SendToUsers = Array.Empty<string>();
         }
 
-        public string Type { get; set; }
+        public string? Type { get; set; }
 
         /// <summary>
-        /// User Ids to not monitor (it's opt out).
+        /// Gets or sets user Ids to not monitor (it's opt out).
         /// </summary>
         public string[] DisabledMonitorUsers { get; set; }
 
         /// <summary>
-        /// User Ids to send to (if SendToUserMode == Custom)
+        /// Gets or sets user Ids to send to (if SendToUserMode == Custom).
         /// </summary>
         public string[] SendToUsers { get; set; }
 

+ 143 - 121
MediaBrowser.Providers/Manager/ProviderManager.cs

@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
@@ -25,7 +23,6 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Events;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
 using Microsoft.Extensions.Logging;
 using Priority_Queue;
 using Book = MediaBrowser.Controller.Entities.Book;
@@ -42,33 +39,38 @@ namespace MediaBrowser.Providers.Manager
     /// </summary>
     public class ProviderManager : IProviderManager, IDisposable
     {
+        private readonly object _refreshQueueLock = new object();
         private readonly ILogger<ProviderManager> _logger;
         private readonly IHttpClient _httpClient;
         private readonly ILibraryMonitor _libraryMonitor;
         private readonly IFileSystem _fileSystem;
         private readonly IServerApplicationPaths _appPaths;
-        private readonly IJsonSerializer _json;
         private readonly ILibraryManager _libraryManager;
         private readonly ISubtitleManager _subtitleManager;
         private readonly IServerConfigurationManager _configurationManager;
+        private readonly ConcurrentDictionary<Guid, double> _activeRefreshes = new ConcurrentDictionary<Guid, double>();
+        private readonly CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
+        private readonly SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>> _refreshQueue =
+            new SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>>();
 
-        private IImageProvider[] ImageProviders { get; set; }
-
-        private IMetadataService[] _metadataServices = { };
-        private IMetadataProvider[] _metadataProviders = { };
+        private IMetadataService[] _metadataServices = Array.Empty<IMetadataService>();
+        private IMetadataProvider[] _metadataProviders = Array.Empty<IMetadataProvider>();
         private IEnumerable<IMetadataSaver> _savers;
-
         private IExternalId[] _externalIds;
-
-        private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
-
-        public event EventHandler<GenericEventArgs<BaseItem>> RefreshStarted;
-        public event EventHandler<GenericEventArgs<BaseItem>> RefreshCompleted;
-        public event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>> RefreshProgress;
+        private bool _isProcessingRefreshQueue;
+        private bool _disposed;
 
         /// <summary>
-        /// Initializes a new instance of the <see cref="ProviderManager" /> class.
+        /// Initializes a new instance of the <see cref="ProviderManager"/> class.
         /// </summary>
+        /// <param name="httpClient">The Http client.</param>
+        /// <param name="subtitleManager">The subtitle manager.</param>
+        /// <param name="configurationManager">The configuration manager.</param>
+        /// <param name="libraryMonitor">The library monitor.</param>
+        /// <param name="logger">The logger.</param>
+        /// <param name="fileSystem">The filesystem.</param>
+        /// <param name="appPaths">The server application paths.</param>
+        /// <param name="libraryManager">The library manager.</param>
         public ProviderManager(
             IHttpClient httpClient,
             ISubtitleManager subtitleManager,
@@ -77,8 +79,7 @@ namespace MediaBrowser.Providers.Manager
             ILogger<ProviderManager> logger,
             IFileSystem fileSystem,
             IServerApplicationPaths appPaths,
-            ILibraryManager libraryManager,
-            IJsonSerializer json)
+            ILibraryManager libraryManager)
         {
             _logger = logger;
             _httpClient = httpClient;
@@ -87,16 +88,27 @@ namespace MediaBrowser.Providers.Manager
             _fileSystem = fileSystem;
             _appPaths = appPaths;
             _libraryManager = libraryManager;
-            _json = json;
             _subtitleManager = subtitleManager;
         }
 
-        /// <summary>
-        /// Adds the metadata providers.
-        /// </summary>
-        public void AddParts(IEnumerable<IImageProvider> imageProviders, IEnumerable<IMetadataService> metadataServices,
-                             IEnumerable<IMetadataProvider> metadataProviders, IEnumerable<IMetadataSaver> metadataSavers,
-                             IEnumerable<IExternalId> externalIds)
+        /// <inheritdoc/>
+        public event EventHandler<GenericEventArgs<BaseItem>> RefreshStarted;
+
+        /// <inheritdoc/>
+        public event EventHandler<GenericEventArgs<BaseItem>> RefreshCompleted;
+
+        /// <inheritdoc/>
+        public event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>> RefreshProgress;
+
+        private IImageProvider[] ImageProviders { get; set; }
+
+        /// <inheritdoc/>
+        public void AddParts(
+            IEnumerable<IImageProvider> imageProviders,
+            IEnumerable<IMetadataService> metadataServices,
+            IEnumerable<IMetadataProvider> metadataProviders,
+            IEnumerable<IMetadataSaver> metadataSavers,
+            IEnumerable<IExternalId> externalIds)
         {
             ImageProviders = imageProviders.ToArray();
 
@@ -104,27 +116,17 @@ namespace MediaBrowser.Providers.Manager
             _metadataProviders = metadataProviders.ToArray();
             _externalIds = externalIds.OrderBy(i => i.ProviderName).ToArray();
 
-            _savers = metadataSavers.Where(i =>
-            {
-                var configurable = i as IConfigurableProvider;
-
-                return configurable == null || configurable.IsEnabled;
-            }).ToArray();
+            _savers = metadataSavers
+                .Where(i => !(i is IConfigurableProvider configurable) || configurable.IsEnabled)
+                .ToArray();
         }
 
+        /// <inheritdoc/>
         public Task<ItemUpdateType> RefreshSingleItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
-            IMetadataService service = null;
             var type = item.GetType();
 
-            foreach (var current in _metadataServices)
-            {
-                if (current.CanRefreshPrimary(type))
-                {
-                    service = current;
-                    break;
-                }
-            }
+            var service = _metadataServices.FirstOrDefault(current => current.CanRefreshPrimary(type));
 
             if (service == null)
             {
@@ -147,35 +149,36 @@ namespace MediaBrowser.Providers.Manager
             return Task.FromResult(ItemUpdateType.None);
         }
 
+        /// <inheritdoc/>
         public async Task SaveImage(BaseItem item, string url, ImageType type, int? imageIndex, CancellationToken cancellationToken)
         {
-            using (var response = await _httpClient.GetResponse(new HttpRequestOptions
+            using var response = await _httpClient.GetResponse(new HttpRequestOptions
             {
                 CancellationToken = cancellationToken,
                 Url = url,
                 BufferContent = false
+            }).ConfigureAwait(false);
 
-            }).ConfigureAwait(false))
+            // Workaround for tvheadend channel icons
+            // TODO: Isolate this hack into the tvh plugin
+            if (string.IsNullOrEmpty(response.ContentType))
             {
-                // Workaround for tvheadend channel icons
-                // TODO: Isolate this hack into the tvh plugin
-                if (string.IsNullOrEmpty(response.ContentType))
+                if (url.IndexOf("/imagecache/", StringComparison.OrdinalIgnoreCase) != -1)
                 {
-                    if (url.IndexOf("/imagecache/", StringComparison.OrdinalIgnoreCase) != -1)
-                    {
-                        response.ContentType = "image/png";
-                    }
+                    response.ContentType = "image/png";
                 }
-
-                await SaveImage(item, response.Content, response.ContentType, type, imageIndex, cancellationToken).ConfigureAwait(false);
             }
+
+            await SaveImage(item, response.Content, response.ContentType, type, imageIndex, cancellationToken).ConfigureAwait(false);
         }
 
+        /// <inheritdoc/>
         public Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken)
         {
             return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, source, mimeType, type, imageIndex, cancellationToken);
         }
 
+        /// <inheritdoc/>
         public Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken)
         {
             if (string.IsNullOrWhiteSpace(source))
@@ -188,12 +191,14 @@ namespace MediaBrowser.Providers.Manager
             return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken);
         }
 
+        /// <inheritdoc/>
         public Task SaveImage(User user, Stream source, string mimeType, string path)
         {
             return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger)
                 .SaveImage(user, source, path);
         }
 
+        /// <inheritdoc/>
         public async Task<IEnumerable<RemoteImageInfo>> GetAvailableRemoteImages(BaseItem item, RemoteImageQuery query, CancellationToken cancellationToken)
         {
             var providers = GetRemoteImageProviders(item, query.IncludeDisabledProviders);
@@ -213,7 +218,7 @@ namespace MediaBrowser.Providers.Manager
                 languages.Add(preferredLanguage);
             }
 
-            var tasks = providers.Select(i => GetImages(item, cancellationToken, i, languages, query.ImageType));
+            var tasks = providers.Select(i => GetImages(item, i, languages, cancellationToken, query.ImageType));
 
             var results = await Task.WhenAll(tasks).ConfigureAwait(false);
 
@@ -224,12 +229,17 @@ namespace MediaBrowser.Providers.Manager
         /// Gets the images.
         /// </summary>
         /// <param name="item">The item.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="provider">The provider.</param>
         /// <param name="preferredLanguages">The preferred languages.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="type">The type.</param>
         /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
-        private async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken, IRemoteImageProvider provider, List<string> preferredLanguages, ImageType? type = null)
+        private async Task<IEnumerable<RemoteImageInfo>> GetImages(
+            BaseItem item,
+            IRemoteImageProvider provider,
+            IReadOnlyCollection<string> preferredLanguages,
+            CancellationToken cancellationToken,
+            ImageType? type = null)
         {
             try
             {
@@ -255,21 +265,23 @@ namespace MediaBrowser.Providers.Manager
             }
             catch (Exception ex)
             {
-                _logger.LogError(ex, "{0} failed in GetImageInfos for type {1}", provider.GetType().Name, item.GetType().Name);
+                _logger.LogError(ex, "{ProviderName} failed in GetImageInfos for type {ItemType} at {ItemPath}", provider.GetType().Name, item.GetType().Name, item.Path);
                 return new List<RemoteImageInfo>();
             }
         }
 
-        /// <summary>
-        /// Gets the supported image providers.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <returns>IEnumerable{IImageProvider}.</returns>
+        /// <inheritdoc/>
         public IEnumerable<ImageProviderInfo> GetRemoteImageProviderInfo(BaseItem item)
         {
             return GetRemoteImageProviders(item, true).Select(i => new ImageProviderInfo(i.Name, i.GetSupportedImages(item).ToArray()));
         }
 
+        /// <summary>
+        /// Gets the image providers for the provided item.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="refreshOptions">The image refresh options.</param>
+        /// <returns>The image providers for the item.</returns>
         public IEnumerable<IImageProvider> GetImageProviders(BaseItem item, ImageRefreshOptions refreshOptions)
         {
             return GetImageProviders(item, _libraryManager.GetLibraryOptions(item), GetMetadataOptions(item), refreshOptions, false);
@@ -283,7 +295,7 @@ namespace MediaBrowser.Providers.Manager
             var typeOptions = libraryOptions.GetTypeOptions(item.GetType().Name);
             var typeFetcherOrder = typeOptions?.ImageFetcherOrder;
 
-            return ImageProviders.Where(i => CanRefresh(i, item, libraryOptions, options, refreshOptions, includeDisabled))
+            return ImageProviders.Where(i => CanRefresh(i, item, libraryOptions, refreshOptions, includeDisabled))
                 .OrderBy(i =>
                 {
                     // See if there's a user-defined order
@@ -304,6 +316,13 @@ namespace MediaBrowser.Providers.Manager
             .ThenBy(GetOrder);
         }
 
+        /// <summary>
+        /// Gets the metadata providers for the provided item.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="libraryOptions">The library options.</param>
+        /// <typeparam name="T">The type of metadata provider.</typeparam>
+        /// <returns>The metadata providers.</returns>
         public IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions)
             where T : BaseItem
         {
@@ -319,7 +338,7 @@ namespace MediaBrowser.Providers.Manager
             var currentOptions = globalMetadataOptions;
 
             return _metadataProviders.OfType<IMetadataProvider<T>>()
-                .Where(i => CanRefresh(i, item, libraryOptions, currentOptions, includeDisabled, forceEnableInternetMetadata))
+                .Where(i => CanRefresh(i, item, libraryOptions, includeDisabled, forceEnableInternetMetadata))
                 .OrderBy(i => GetConfiguredOrder(item, i, libraryOptions, globalMetadataOptions))
                 .ThenBy(GetDefaultOrder);
         }
@@ -329,14 +348,20 @@ namespace MediaBrowser.Providers.Manager
             var options = GetMetadataOptions(item);
             var libraryOptions = _libraryManager.GetLibraryOptions(item);
 
-            return GetImageProviders(item, libraryOptions, options,
-                    new ImageRefreshOptions(
-                        new DirectoryService(_fileSystem)),
-                    includeDisabled)
-                .OfType<IRemoteImageProvider>();
+            return GetImageProviders(
+                item,
+                libraryOptions,
+                options,
+                new ImageRefreshOptions(new DirectoryService(_fileSystem)),
+                includeDisabled).OfType<IRemoteImageProvider>();
         }
 
-        private bool CanRefresh(IMetadataProvider provider, BaseItem item, LibraryOptions libraryOptions, MetadataOptions options, bool includeDisabled, bool forceEnableInternetMetadata)
+        private bool CanRefresh(
+            IMetadataProvider provider,
+            BaseItem item,
+            LibraryOptions libraryOptions,
+            bool includeDisabled,
+            bool forceEnableInternetMetadata)
         {
             if (!includeDisabled)
             {
@@ -372,7 +397,12 @@ namespace MediaBrowser.Providers.Manager
             return true;
         }
 
-        private bool CanRefresh(IImageProvider provider, BaseItem item, LibraryOptions libraryOptions, MetadataOptions options, ImageRefreshOptions refreshOptions, bool includeDisabled)
+        private bool CanRefresh(
+            IImageProvider provider,
+            BaseItem item,
+            LibraryOptions libraryOptions,
+            ImageRefreshOptions refreshOptions,
+            bool includeDisabled)
         {
             if (!includeDisabled)
             {
@@ -400,7 +430,7 @@ namespace MediaBrowser.Providers.Manager
             }
             catch (Exception ex)
             {
-                _logger.LogError(ex, "{0} failed in Supports for type {1}", provider.GetType().Name, item.GetType().Name);
+                _logger.LogError(ex, "{ProviderName} failed in Supports for type {ItemType} at {ItemPath}", provider.GetType().Name, item.GetType().Name, item.Path);
                 return false;
             }
         }
@@ -412,9 +442,7 @@ namespace MediaBrowser.Providers.Manager
         /// <returns>System.Int32.</returns>
         private int GetOrder(IImageProvider provider)
         {
-            var hasOrder = provider as IHasOrder;
-
-            if (hasOrder == null)
+            if (!(provider is IHasOrder hasOrder))
             {
                 return 0;
             }
@@ -441,7 +469,7 @@ namespace MediaBrowser.Providers.Manager
             if (provider is IRemoteMetadataProvider)
             {
                 var typeOptions = libraryOptions.GetTypeOptions(item.GetType().Name);
-                var typeFetcherOrder = typeOptions == null ? null : typeOptions.MetadataFetcherOrder;
+                var typeFetcherOrder = typeOptions?.MetadataFetcherOrder;
 
                 var fetcherOrder = typeFetcherOrder ?? globalMetadataOptions.MetadataFetcherOrder;
 
@@ -459,9 +487,7 @@ namespace MediaBrowser.Providers.Manager
 
         private int GetDefaultOrder(IMetadataProvider provider)
         {
-            var hasOrder = provider as IHasOrder;
-
-            if (hasOrder != null)
+            if (provider is IHasOrder hasOrder)
             {
                 return hasOrder.Order;
             }
@@ -469,9 +495,10 @@ namespace MediaBrowser.Providers.Manager
             return 0;
         }
 
+        /// <inheritdoc/>
         public MetadataPluginSummary[] GetAllMetadataPlugins()
         {
-            return new MetadataPluginSummary[]
+            return new[]
             {
                 GetPluginSummary<Movie>(),
                 GetPluginSummary<BoxSet>(),
@@ -493,7 +520,7 @@ namespace MediaBrowser.Providers.Manager
             where T : BaseItem, new()
         {
             // Give it a dummy path just so that it looks like a file system item
-            var dummy = new T()
+            var dummy = new T
             {
                 Path = Path.Combine(_appPaths.InternalMetadataPath, "dummy"),
                 ParentId = Guid.NewGuid()
@@ -508,11 +535,12 @@ namespace MediaBrowser.Providers.Manager
 
             var libraryOptions = new LibraryOptions();
 
-            var imageProviders = GetImageProviders(dummy, libraryOptions, options,
-                                    new ImageRefreshOptions(
-                                        new DirectoryService(_fileSystem)),
-                                    true)
-                                .ToList();
+            var imageProviders = GetImageProviders(
+                dummy,
+                libraryOptions,
+                options,
+                new ImageRefreshOptions(new DirectoryService(_fileSystem)),
+                true).ToList();
 
             var pluginList = summary.Plugins.ToList();
 
@@ -572,7 +600,6 @@ namespace MediaBrowser.Providers.Manager
         private void AddImagePlugins<T>(List<MetadataPlugin> list, T item, List<IImageProvider> imageProviders)
             where T : BaseItem
         {
-
             // Locals
             list.AddRange(imageProviders.Where(i => (i is ILocalImageProvider)).Select(i => new MetadataPlugin
             {
@@ -588,6 +615,7 @@ namespace MediaBrowser.Providers.Manager
             }));
         }
 
+        /// <inheritdoc/>
         public MetadataOptions GetMetadataOptions(BaseItem item)
         {
             var type = item.GetType().Name;
@@ -597,17 +625,13 @@ namespace MediaBrowser.Providers.Manager
                 new MetadataOptions();
         }
 
-        /// <summary>
-        /// Saves the metadata.
-        /// </summary>
+        /// <inheritdoc/>
         public void SaveMetadata(BaseItem item, ItemUpdateType updateType)
         {
             SaveMetadata(item, updateType, _savers);
         }
 
-        /// <summary>
-        /// Saves the metadata.
-        /// </summary>
+        /// <inheritdoc/>
         public void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<string> savers)
         {
             SaveMetadata(item, updateType, _savers.Where(i => savers.Contains(i.Name, StringComparer.OrdinalIgnoreCase)));
@@ -619,7 +643,6 @@ namespace MediaBrowser.Providers.Manager
         /// <param name="item">The item.</param>
         /// <param name="updateType">Type of the update.</param>
         /// <param name="savers">The savers.</param>
-        /// <returns>Task.</returns>
         private void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<IMetadataSaver> savers)
         {
             var libraryOptions = _libraryManager.GetLibraryOptions(item);
@@ -628,11 +651,9 @@ namespace MediaBrowser.Providers.Manager
             {
                 _logger.LogDebug("Saving {0} to {1}.", item.Path ?? item.Name, saver.Name);
 
-                var fileSaver = saver as IMetadataFileSaver;
-
-                if (fileSaver != null)
+                if (saver is IMetadataFileSaver fileSaver)
                 {
-                    string path = null;
+                    string path;
 
                     try
                     {
@@ -699,11 +720,9 @@ namespace MediaBrowser.Providers.Manager
                         {
                             if (updateType >= ItemUpdateType.MetadataEdit)
                             {
-                                var fileSaver = saver as IMetadataFileSaver;
-
                                 // Manual edit occurred
                                 // Even if save local is off, save locally anyway if the metadata file already exists
-                                if (fileSaver == null || !File.Exists(fileSaver.GetSavePath(item)))
+                                if (!(saver is IMetadataFileSaver fileSaver) || !File.Exists(fileSaver.GetSavePath(item)))
                                 {
                                     return false;
                                 }
@@ -734,6 +753,7 @@ namespace MediaBrowser.Providers.Manager
             }
         }
 
+        /// <inheritdoc/>
         public Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>(RemoteSearchQuery<TLookupType> searchInfo, CancellationToken cancellationToken)
             where TItemType : BaseItem, new()
             where TLookupType : ItemLookupInfo
@@ -748,7 +768,7 @@ namespace MediaBrowser.Providers.Manager
             return GetRemoteSearchResults<TItemType, TLookupType>(searchInfo, referenceItem, cancellationToken);
         }
 
-        public async Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>(RemoteSearchQuery<TLookupType> searchInfo, BaseItem referenceItem, CancellationToken cancellationToken)
+        private async Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>(RemoteSearchQuery<TLookupType> searchInfo, BaseItem referenceItem, CancellationToken cancellationToken)
             where TItemType : BaseItem, new()
             where TLookupType : ItemLookupInfo
         {
@@ -837,7 +857,9 @@ namespace MediaBrowser.Providers.Manager
             return resultList;
         }
 
-        private async Task<IEnumerable<RemoteSearchResult>> GetSearchResults<TLookupType>(IRemoteSearchProvider<TLookupType> provider, TLookupType searchInfo,
+        private async Task<IEnumerable<RemoteSearchResult>> GetSearchResults<TLookupType>(
+            IRemoteSearchProvider<TLookupType> provider,
+            TLookupType searchInfo,
             CancellationToken cancellationToken)
             where TLookupType : ItemLookupInfo
         {
@@ -853,6 +875,7 @@ namespace MediaBrowser.Providers.Manager
             return list;
         }
 
+        /// <inheritdoc/>
         public Task<HttpResponseInfo> GetSearchImage(string providerName, string url, CancellationToken cancellationToken)
         {
             var provider = _metadataProviders.OfType<IRemoteSearchProvider>().FirstOrDefault(i => string.Equals(i.Name, providerName, StringComparison.OrdinalIgnoreCase));
@@ -865,6 +888,7 @@ namespace MediaBrowser.Providers.Manager
             return provider.GetImageResponse(url, cancellationToken);
         }
 
+        /// <inheritdoc/>
         public IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item)
         {
             return _externalIds.Where(i =>
@@ -881,6 +905,7 @@ namespace MediaBrowser.Providers.Manager
             });
         }
 
+        /// <inheritdoc/>
         public IEnumerable<ExternalUrl> GetExternalUrls(BaseItem item)
         {
             return GetExternalIds(item)
@@ -909,6 +934,7 @@ namespace MediaBrowser.Providers.Manager
             }).Where(i => i != null).Concat(item.GetRelatedUrls());
         }
 
+        /// <inheritdoc/>
         public IEnumerable<ExternalIdInfo> GetExternalIdInfos(IHasProviderIds item)
         {
             return GetExternalIds(item)
@@ -921,8 +947,7 @@ namespace MediaBrowser.Providers.Manager
                 });
         }
 
-        private ConcurrentDictionary<Guid, double> _activeRefreshes = new ConcurrentDictionary<Guid, double>();
-
+        /// <inheritdoc/>
         public Dictionary<Guid, Guid> GetRefreshQueue()
         {
             lock (_refreshQueueLock)
@@ -938,6 +963,7 @@ namespace MediaBrowser.Providers.Manager
             }
         }
 
+        /// <inheritdoc/>
         public void OnRefreshStart(BaseItem item)
         {
             _logger.LogInformation("OnRefreshStart {0}", item.Id.ToString("N", CultureInfo.InvariantCulture));
@@ -945,6 +971,7 @@ namespace MediaBrowser.Providers.Manager
             RefreshStarted?.Invoke(this, new GenericEventArgs<BaseItem>(item));
         }
 
+        /// <inheritdoc/>
         public void OnRefreshComplete(BaseItem item)
         {
             _logger.LogInformation("OnRefreshComplete {0}", item.Id.ToString("N", CultureInfo.InvariantCulture));
@@ -954,6 +981,7 @@ namespace MediaBrowser.Providers.Manager
             RefreshCompleted?.Invoke(this, new GenericEventArgs<BaseItem>(item));
         }
 
+        /// <inheritdoc/>
         public double? GetRefreshProgress(Guid id)
         {
             if (_activeRefreshes.TryGetValue(id, out double value))
@@ -964,6 +992,7 @@ namespace MediaBrowser.Providers.Manager
             return null;
         }
 
+        /// <inheritdoc/>
         public void OnRefreshProgress(BaseItem item, double progress)
         {
             var id = item.Id;
@@ -983,12 +1012,7 @@ namespace MediaBrowser.Providers.Manager
             RefreshProgress?.Invoke(this, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(item, progress)));
         }
 
-        private readonly SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>> _refreshQueue =
-            new SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>>();
-
-        private readonly object _refreshQueueLock = new object();
-        private bool _isProcessingRefreshQueue;
-
+        /// <inheritdoc/>
         public void QueueRefresh(Guid id, MetadataRefreshOptions options, RefreshPriority priority)
         {
             if (_disposed)
@@ -1032,7 +1056,7 @@ namespace MediaBrowser.Providers.Manager
                     if (item != null)
                     {
                         // Try to throttle this a little bit.
-                        await Task.Delay(100).ConfigureAwait(false);
+                        await Task.Delay(100, cancellationToken).ConfigureAwait(false);
 
                         var task = item is MusicArtist artist
                             ? RefreshArtist(artist, refreshItem.Item2, cancellationToken)
@@ -1062,17 +1086,14 @@ namespace MediaBrowser.Providers.Manager
             await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
 
             // Collection folders don't validate their children so we'll have to simulate that here
-
-            if (item is CollectionFolder collectionFolder)
+            switch (item)
             {
-                await RefreshCollectionFolderChildren(options, collectionFolder, cancellationToken).ConfigureAwait(false);
-            }
-            else
-            {
-                if (item is Folder folder)
-                {
+                case CollectionFolder collectionFolder:
+                    await RefreshCollectionFolderChildren(options, collectionFolder, cancellationToken).ConfigureAwait(false);
+                    break;
+                case Folder folder:
                     await folder.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options).ConfigureAwait(false);
-                }
+                    break;
             }
         }
 
@@ -1082,7 +1103,7 @@ namespace MediaBrowser.Providers.Manager
             {
                 await child.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
 
-                await child.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options, true).ConfigureAwait(false);
+                await child.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options).ConfigureAwait(false);
             }
         }
 
@@ -1118,12 +1139,13 @@ namespace MediaBrowser.Providers.Manager
             }
         }
 
+        /// <inheritdoc/>
         public Task RefreshFullItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
             return RefreshItem(item, options, cancellationToken);
         }
 
-        private bool _disposed;
+        /// <inheritdoc/>
         public void Dispose()
         {
             _disposed = true;

+ 2 - 2
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -16,8 +16,8 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" />
-    <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.5" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
+    <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.6" />
     <PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" />
     <PackageReference Include="PlaylistsNET" Version="1.0.6" />
     <PackageReference Include="TvDbSharper" Version="3.2.0" />

+ 22 - 20
MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html

@@ -28,29 +28,31 @@
                 pluginId: "a629c0da-fac5-4c7e-931a-7174223f14c8"
             };
 
-            $('.configPage').on('pageshow', function () {
-                Dashboard.showLoadingMsg();
-                ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
-                    $('#enable').checked = config.Enable;
-                    $('#replaceAlbumName').checked = config.ReplaceAlbumName;
-
-                    Dashboard.hideLoadingMsg();
+            document.querySelector('.configPage')
+                .addEventListener('pageshow', function () {
+                    Dashboard.showLoadingMsg();
+                    ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
+                        document.querySelector('#enable').checked = config.Enable;
+                        document.querySelector('#replaceAlbumName').checked = config.ReplaceAlbumName;
+    
+                        Dashboard.hideLoadingMsg();
+                    });
                 });
-            });
-
-            $('.configForm').on('submit', function (e) {
-                Dashboard.showLoadingMsg();
 
-                var form = this;
-                ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
-                    config.Enable = $('#enable', form).checked;
-                    config.ReplaceAlbumName = $('#replaceAlbumName', form).checked;
-
-                    ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+            document.querySelector('.configForm')
+                .addEventListener('submit', function (e) {
+                    Dashboard.showLoadingMsg();
+    
+                    ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
+                        config.Enable = document.querySelector('#enable').checked;
+                        config.ReplaceAlbumName = document.querySelector('#replaceAlbumName').checked;
+    
+                        ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+                    });
+                    
+                    e.preventDefault();
+                    return false;
                 });
-
-                return false;
-            });
         </script>
     </div>
 </body>

+ 38 - 24
MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html

@@ -36,33 +36,47 @@
                 uniquePluginId: "8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a"
             };
 
-            $('.musicBrainzConfigPage').on('pageshow', function () {
-                Dashboard.showLoadingMsg();
-                ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
-                    $('#server').val(config.Server).change();
-                    $('#rateLimit').val(config.RateLimit).change();
-                    $('#enable').checked = config.Enable;
-                    $('#replaceArtistName').checked = config.ReplaceArtistName;
+            document.querySelector('.musicBrainzConfigPage')
+                .addEventListener('pageshow', function () {
+                    Dashboard.showLoadingMsg();
+                    ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
+                        var server = document.querySelector('#server');
+                        server.value = config.Server;
+                        server.dispatchEvent(new Event('change', {
+                            bubbles: true,
+                            cancelable: false
+                        }));
+                        
+                        var rateLimit = document.querySelector('#rateLimit');
+                        rateLimit.value = config.RateLimit;
+                        rateLimit.dispatchEvent(new Event('change', {
+                            bubbles: true,
+                            cancelable: false
+                        }));
+                        
+                        document.querySelector('#enable').checked = config.Enable;
+                        document.querySelector('#replaceArtistName').checked = config.ReplaceArtistName;
 
-                    Dashboard.hideLoadingMsg();
+                        Dashboard.hideLoadingMsg();
+                    });
                 });
-            });
-
-            $('.musicBrainzConfigForm').on('submit', function (e) {
-                Dashboard.showLoadingMsg();
-
-                var form = this;
-                ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
-                    config.Server = $('#server', form).val();
-                    config.RateLimit = $('#rateLimit', form).val();
-                    config.Enable = $('#enable', form).checked;
-                    config.ReplaceArtistName = $('#replaceArtistName', form).checked;
-
-                    ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+            
+            document.querySelector('.musicBrainzConfigForm')
+                .addEventListener('submit', function (e) {
+                    Dashboard.showLoadingMsg();
+    
+                    ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
+                        config.Server = document.querySelector('#server').value;
+                        config.RateLimit = document.querySelector('#rateLimit').value;
+                        config.Enable = document.querySelector('#enable').checked;
+                        config.ReplaceArtistName = document.querySelector('#replaceArtistName').checked;
+    
+                        ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+                    });
+                    
+                    e.preventDefault();
+                    return false;
                 });
-
-                return false;
-            });
         </script>
     </div>
 </body>

+ 19 - 16
MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html

@@ -24,25 +24,28 @@
                 pluginId: "a628c0da-fac5-4c7e-9d1a-7134223f14c8"
             };
 
-            $('.configPage').on('pageshow', function () {
-                Dashboard.showLoadingMsg();
-                ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
-                    $('#castAndCrew').checked = config.CastAndCrew;
-                    Dashboard.hideLoadingMsg();
+            document.querySelector('.configPage')
+                .addEventListener('pageshow', function () {
+                    Dashboard.showLoadingMsg();
+                    ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
+                        document.querySelector('#castAndCrew').checked = config.CastAndCrew;
+                        Dashboard.hideLoadingMsg();
+                    });
                 });
-            });
 
-            $('.configForm').on('submit', function (e) {
-                Dashboard.showLoadingMsg();
-
-                var form = this;
-                ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
-                    config.CastAndCrew = $('#castAndCrew', form).checked;
-                    ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+            
+            document.querySelector('.configForm')
+                .addEventListener('submit', function (e) {
+                    Dashboard.showLoadingMsg();
+    
+                    ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
+                        config.CastAndCrew = document.querySelector('#castAndCrew').checked;
+                        ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+                    });
+                    
+                    e.preventDefault();
+                    return false;
                 });
-
-                return false;
-            });
         </script>
     </div>
 </body>

+ 40 - 0
MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs

@@ -4,6 +4,7 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Reflection;
+using System.Runtime.CompilerServices;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Controller.Providers;
@@ -229,6 +230,45 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
             return GetEpisodesPageAsync(tvdbId, 1, episodeQuery, language, cancellationToken);
         }
 
+        public async IAsyncEnumerable<KeyType> GetImageKeyTypesForSeriesAsync(int tvdbId, string language, [EnumeratorCancellation] CancellationToken cancellationToken)
+        {
+            var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId);
+            var imagesSummary = await TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false);
+
+            if (imagesSummary.Data.Fanart > 0)
+            {
+                yield return KeyType.Fanart;
+            }
+
+            if (imagesSummary.Data.Series > 0)
+            {
+                yield return KeyType.Series;
+            }
+
+            if (imagesSummary.Data.Poster > 0)
+            {
+                yield return KeyType.Poster;
+            }
+        }
+
+        public async IAsyncEnumerable<KeyType> GetImageKeyTypesForSeasonAsync(int tvdbId, string language, [EnumeratorCancellation] CancellationToken cancellationToken)
+        {
+            var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId);
+            var imagesSummary = await TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false);
+
+            if (imagesSummary.Data.Season > 0)
+            {
+                yield return KeyType.Season;
+            }
+
+            if (imagesSummary.Data.Fanart > 0)
+            {
+                yield return KeyType.Fanart;
+            }
+
+            // TODO seasonwide is not supported in TvDbSharper
+        }
+
         private async Task<T> TryGetValue<T>(string key, string language, Func<Task<T>> resultFactory)
         {
             if (_cache.TryGetValue(key, out T cachedValue))

+ 2 - 2
MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs

@@ -65,8 +65,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
             var language = item.GetPreferredMetadataLanguage();
             var remoteImages = new List<RemoteImageInfo>();
 
-            var keyTypes = new[] { KeyType.Season, KeyType.Seasonwide, KeyType.Fanart };
-            foreach (var keyType in keyTypes)
+            var keyTypes = _tvdbClientManager.GetImageKeyTypesForSeasonAsync(tvdbId, language, cancellationToken).ConfigureAwait(false);
+            await foreach (var keyType in keyTypes)
             {
                 var imageQuery = new ImagesQuery
                 {

+ 3 - 2
MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs

@@ -59,9 +59,10 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
 
             var language = item.GetPreferredMetadataLanguage();
             var remoteImages = new List<RemoteImageInfo>();
-            var keyTypes = new[] { KeyType.Poster, KeyType.Series, KeyType.Fanart };
             var tvdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tvdb));
-            foreach (KeyType keyType in keyTypes)
+            var allowedKeyTypes = _tvdbClientManager.GetImageKeyTypesForSeriesAsync(tvdbId, language, cancellationToken)
+                .ConfigureAwait(false);
+            await foreach (KeyType keyType in allowedKeyTypes)
             {
                 var imageQuery = new ImagesQuery
                 {

+ 12 - 3
MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs

@@ -247,10 +247,15 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
                 {
                     Name = tvdbTitles.FirstOrDefault(),
                     ProductionYear = firstAired.Year,
-                    SearchProviderName = Name,
-                    ImageUrl = TvdbUtils.BannerUrl + seriesSearchResult.Banner
+                    SearchProviderName = Name
                 };
 
+                if (!string.IsNullOrEmpty(seriesSearchResult.Banner))
+                {
+                    // Results from their Search endpoints already include the /banners/ part in the url, because reasons...
+                    remoteSearchResult.ImageUrl = TvdbUtils.TvdbImageBaseUrl + seriesSearchResult.Banner;
+                }
+
                 try
                 {
                     var seriesSesult =
@@ -365,10 +370,14 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
                     Type = PersonType.Actor,
                     Name = (actor.Name ?? string.Empty).Trim(),
                     Role = actor.Role,
-                    ImageUrl = TvdbUtils.BannerUrl + actor.Image,
                     SortOrder = actor.SortOrder
                 };
 
+                if (!string.IsNullOrEmpty(actor.Image))
+                {
+                    personInfo.ImageUrl = TvdbUtils.BannerUrl + actor.Image;
+                }
+
                 if (!string.IsNullOrWhiteSpace(personInfo.Name))
                 {
                     result.AddPerson(personInfo);

+ 2 - 1
MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs

@@ -9,7 +9,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
     {
         public const string TvdbApiKey = "OG4V3YJ3FAP7FP2K";
         public const string TvdbBaseUrl = "https://www.thetvdb.com/";
-        public const string BannerUrl = TvdbBaseUrl + "banners/";
+        public const string TvdbImageBaseUrl = "https://www.thetvdb.com";
+        public const string BannerUrl = TvdbImageBaseUrl + "/banners/";
 
         public static ImageType GetImageTypeFromKeyType(string keyType)
         {

+ 1 - 1
MediaBrowser.Providers/TV/SeriesMetadataService.cs

@@ -58,7 +58,7 @@ namespace MediaBrowser.Providers.TV
             }
             catch (Exception ex)
             {
-                Logger.LogError(ex, "Error in DummySeasonProvider");
+                Logger.LogError(ex, "Error in DummySeasonProvider for {ItemPath}", item.Path);
             }
         }
 

+ 6 - 0
debian/changelog

@@ -1,3 +1,9 @@
+jellyfin-server (10.6.0-2) unstable; urgency=medium
+
+  * Fix upgrade bug
+
+ -- Joshua Boniface <joshua@boniface.me>  Sun, 19 Jul 22:47:27 -0400
+
 jellyfin-server (10.6.0-1) unstable; urgency=medium
 
   * Forthcoming stable release

+ 2 - 2
debian/conf/jellyfin

@@ -31,7 +31,7 @@ JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
 #JELLYFIN_SERVICE_OPT="--service"
 
 # [OPTIONAL] run Jellyfin without the web app
-#JELLYFIN_NOWEBAPP_OPT="--noautorunwebapp"
+#JELLYFIN_NOWEBAPP_OPT="--nowebclient"
 
 #
 # SysV init/Upstart options
@@ -40,4 +40,4 @@ JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
 # Application username
 JELLYFIN_USER="jellyfin"
 # Full application command
-JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLFIN_NOWEBAPP_OPT"
+JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT"

+ 2 - 3
debian/control

@@ -15,9 +15,8 @@ Vcs-Git: https://github.org/jellyfin/jellyfin.git
 Vcs-Browser: https://github.org/jellyfin/jellyfin
 
 Package: jellyfin-server
-Replaces: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server
-Breaks: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server
-Conflicts: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server
+Replaces: jellyfin (<<10.6.0)
+Breaks: jellyfin (<<10.6.0)
 Architecture: any
 Depends: at,
          libsqlite3-0,

+ 4 - 6
deployment/build.windows.amd64

@@ -8,8 +8,7 @@ set -o xtrace
 # Version variables
 NSSM_VERSION="nssm-2.24-101-g897c7ad"
 NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip"
-FFMPEG_VERSION="ffmpeg-4.3-win64-static"
-FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win64/static/${FFMPEG_VERSION}.zip"
+FFMPEG_URL="https://repo.jellyfin.org/releases/server/windows/ffmpeg/jellyfin-ffmpeg.zip";
 
 # Move to source directory
 pushd ${SOURCE_DIR}
@@ -29,12 +28,11 @@ dotnet publish Jellyfin.Server --configuration Release --self-contained --runtim
 # Prepare addins
 addin_build_dir="$( mktemp -d )"
 wget ${NSSM_URL} -O ${addin_build_dir}/nssm.zip
-wget ${FFMPEG_URL} -O ${addin_build_dir}/ffmpeg.zip
+wget ${FFMPEG_URL} -O ${addin_build_dir}/jellyfin-ffmpeg.zip
 unzip ${addin_build_dir}/nssm.zip -d ${addin_build_dir}
 cp ${addin_build_dir}/${NSSM_VERSION}/win64/nssm.exe ${output_dir}/nssm.exe
-unzip ${addin_build_dir}/ffmpeg.zip -d ${addin_build_dir}
-cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffmpeg.exe ${output_dir}/ffmpeg.exe
-cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffprobe.exe ${output_dir}/ffprobe.exe
+unzip ${addin_build_dir}/jellyfin-ffmpeg.zip -d ${addin_build_dir}/jellyfin-ffmpeg
+cp ${addin_build_dir}/jellyfin-ffmpeg/* ${output_dir}
 rm -rf ${addin_build_dir}
 
 # Prepare scripts

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

@@ -16,7 +16,7 @@
     <PackageReference Include="AutoFixture" Version="4.13.0" />
     <PackageReference Include="AutoFixture.AutoMoq" Version="4.12.0" />
     <PackageReference Include="AutoFixture.Xunit2" Version="4.12.0" />
-    <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.5" />
+    <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.6" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />

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

@@ -8,7 +8,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.5" />
+    <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.6" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />

+ 1 - 1
windows/build-jellyfin.ps1

@@ -47,7 +47,7 @@ function Install-FFMPEG {
     param(
         [string]$ResolvedInstallLocation,
         [string]$Architecture,
-        [string]$FFMPEGVersionX86 = "ffmpeg-4.2.1-win32-shared"
+        [string]$FFMPEGVersionX86 = "ffmpeg-4.3-win32-shared"
     )
     Write-Verbose "Checking Architecture"
     if($Architecture -notin @('x86','x64')){