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)
         sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)
         rm $0
         rm $0
         exit
         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.Dlna;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Services;
 using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
 
 
 namespace Emby.Dlna.Api
 namespace Emby.Dlna.Api
 {
 {
@@ -108,7 +109,7 @@ namespace Emby.Dlna.Api
         public string Filename { get; set; }
         public string Filename { get; set; }
     }
     }
 
 
-    public class DlnaServerService : IService, IRequiresRequest
+    public class DlnaServerService : IService
     {
     {
         private const string XMLContentType = "text/xml; charset=UTF-8";
         private const string XMLContentType = "text/xml; charset=UTF-8";
 
 
@@ -127,11 +128,13 @@ namespace Emby.Dlna.Api
         public DlnaServerService(
         public DlnaServerService(
             IDlnaManager dlnaManager,
             IDlnaManager dlnaManager,
             IHttpResultFactory httpResultFactory,
             IHttpResultFactory httpResultFactory,
-            IServerConfigurationManager configurationManager)
+            IServerConfigurationManager configurationManager,
+            IHttpContextAccessor httpContextAccessor)
         {
         {
             _dlnaManager = dlnaManager;
             _dlnaManager = dlnaManager;
             _resultFactory = httpResultFactory;
             _resultFactory = httpResultFactory;
             _configurationManager = configurationManager;
             _configurationManager = configurationManager;
+            Request = httpContextAccessor?.HttpContext.GetServiceStackRequest() ?? throw new ArgumentNullException(nameof(httpContextAccessor));
         }
         }
 
 
         private string GetHeader(string name)
         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\">");
             builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
             foreach (var key in stateVariables.Keys)
             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>");
             builder.Append("</e:propertyset>");

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

@@ -4,12 +4,12 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
+using System.Security;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using System.Xml;
 using System.Xml;
 using System.Xml.Linq;
 using System.Xml.Linq;
 using Emby.Dlna.Common;
 using Emby.Dlna.Common;
-using Emby.Dlna.Server;
 using Emby.Dlna.Ssdp;
 using Emby.Dlna.Ssdp;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
@@ -334,7 +334,7 @@ namespace Emby.Dlna.PlayTo
                 return string.Empty;
                 return string.Empty;
             }
             }
 
 
-            return DescriptionXmlBuilder.Escape(value);
+            return SecurityElement.Escape(value);
         }
         }
 
 
         private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)
         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.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
+using System.Security;
 using System.Text;
 using System.Text;
 using Emby.Dlna.Common;
 using Emby.Dlna.Common;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dlna;
@@ -64,10 +65,10 @@ namespace Emby.Dlna.Server
 
 
             foreach (var att in attributes)
             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("<specVersion>");
             builder.Append("<major>1</major>");
             builder.Append("<major>1</major>");
@@ -76,7 +77,9 @@ namespace Emby.Dlna.Server
 
 
             if (!EnableAbsoluteUrls)
             if (!EnableAbsoluteUrls)
             {
             {
-                builder.Append("<URLBase>" + Escape(_serverAddress) + "</URLBase>");
+                builder.Append("<URLBase>")
+                    .Append(SecurityElement.Escape(_serverAddress))
+                    .Append("</URLBase>");
             }
             }
 
 
             AppendDeviceInfo(builder);
             AppendDeviceInfo(builder);
@@ -93,91 +96,14 @@ namespace Emby.Dlna.Server
 
 
             AppendIconList(builder);
             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);
             AppendServiceList(builder);
             builder.Append("</device>");
             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)
         private void AppendDeviceProperties(StringBuilder builder)
         {
         {
             builder.Append("<dlna:X_DLNACAP/>");
             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("<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))
             if (string.IsNullOrEmpty(_profile.SerialNumber))
             {
             {
-                builder.Append("<serialNumber>" + Escape(_serverId) + "</serialNumber>");
+                builder.Append("<serialNumber>")
+                    .Append(SecurityElement.Escape(_serverId))
+                    .Append("</serialNumber>");
             }
             }
             else
             else
             {
             {
-                builder.Append("<serialNumber>" + Escape(_profile.SerialNumber) + "</serialNumber>");
+                builder.Append("<serialNumber>")
+                    .Append(SecurityElement.Escape(_profile.SerialNumber))
+                    .Append("</serialNumber>");
             }
             }
 
 
             builder.Append("<UPC/>");
             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))
             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("<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>");
                 builder.Append("</icon>");
             }
             }
@@ -270,11 +228,21 @@ namespace Emby.Dlna.Server
             {
             {
                 builder.Append("<service>");
                 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>");
                 builder.Append("</service>");
             }
             }
@@ -298,7 +266,7 @@ namespace Emby.Dlna.Server
                 url = _serverAddress.TrimEnd('/') + url;
                 url = _serverAddress.TrimEnd('/') + url;
             }
             }
 
 
-            return Escape(url);
+            return SecurityElement.Escape(url);
         }
         }
 
 
         private IEnumerable<DeviceIcon> GetIcons()
         private IEnumerable<DeviceIcon> GetIcons()

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

@@ -1,9 +1,9 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Security;
 using System.Text;
 using System.Text;
 using Emby.Dlna.Common;
 using Emby.Dlna.Common;
-using Emby.Dlna.Server;
 
 
 namespace Emby.Dlna.Service
 namespace Emby.Dlna.Service
 {
 {
@@ -37,7 +37,9 @@ namespace Emby.Dlna.Service
             {
             {
                 builder.Append("<action>");
                 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>");
                 builder.Append("<argumentList>");
 
 
@@ -45,9 +47,15 @@ namespace Emby.Dlna.Service
                 {
                 {
                     builder.Append("<argument>");
                     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>");
                     builder.Append("</argument>");
                 }
                 }
@@ -68,17 +76,25 @@ namespace Emby.Dlna.Service
             {
             {
                 var sendEvents = item.SendsEvents ? "yes" : "no";
                 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)
                 if (item.AllowedValues.Length > 0)
                 {
                 {
                     builder.Append("<allowedValueList>");
                     builder.Append("<allowedValueList>");
                     foreach (var allowedValue in item.AllowedValues)
                     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>");
                     builder.Append("</allowedValueList>");

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

@@ -136,8 +136,8 @@ namespace Emby.Naming.Common
 
 
             CleanDateTimes = new[]
             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[]
             CleanStrings = new[]
@@ -277,7 +277,7 @@ namespace Emby.Naming.Common
                 // This isn't a Kodi naming rule, but the expression below causes false positives,
                 // This isn't a Kodi naming rule, but the expression below causes false positives,
                 // so we make sure this one gets tested first.
                 // so we make sure this one gets tested first.
                 // "Foo Bar 889"
                 // "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
                     IsNamed = true
                 },
                 },
@@ -300,32 +300,32 @@ namespace Emby.Naming.Common
                 // *** End Kodi Standard Naming
                 // *** End Kodi Standard Naming
 
 
                 // [bar] Foo - 1 [baz]
                 // [bar] Foo - 1 [baz]
-                new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>\d+).*$")
+                new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[-\s_]+(?<epnumber>[0-9]+).*$")
                 {
                 {
                     IsNamed = true
                     IsNamed = true
                 },
                 },
-                new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>\d+)[xX](?<epnumber>\d+)[^\\\/]*$")
+                new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
                 {
                 {
                     IsNamed = true
                     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
                     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
                     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
                     IsNamed = true
                 },
                 },
 
 
                 // "01.avi"
                 // "01.avi"
-                new EpisodeExpression(@".*[\\\/](?<epnumber>\d+)(-(?<endingepnumber>\d+))*\.\w+$")
+                new EpisodeExpression(@".*[\\\/](?<epnumber>[0-9]+)(-(?<endingepnumber>[0-9]+))*\.\w+$")
                 {
                 {
                     IsOptimistic = true,
                     IsOptimistic = true,
                     IsNamed = true
                     IsNamed = true
@@ -335,34 +335,34 @@ namespace Emby.Naming.Common
                 new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
                 new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
 
 
                 // "01 - blah.avi", "01-blah.avi"
                 // "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,
                     IsOptimistic = true,
                     IsNamed = true
                     IsNamed = true
                 },
                 },
 
 
                 // "01.blah.avi"
                 // "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,
                     IsOptimistic = true,
                     IsNamed = 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"
                 // "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,
                     IsOptimistic = true,
                     IsNamed = true
                     IsNamed = true
                 },
                 },
 
 
                 // "01 episode title.avi"
                 // "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,
                     IsOptimistic = true,
                     IsNamed = true
                     IsNamed = true
                 },
                 },
                 // "Episode 16", "Episode 16 - Title"
                 // "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,
                     IsOptimistic = true,
                     IsNamed = true
                     IsNamed = true
@@ -625,17 +625,17 @@ namespace Emby.Naming.Common
             AudioBookPartsExpressions = new[]
             AudioBookPartsExpressions = new[]
             {
             {
                 // Detect specified chapters, like CH 01
                 // Detect specified chapters, like CH 01
-                @"ch(?:apter)?[\s_-]?(?<chapter>\d+)",
+                @"ch(?:apter)?[\s_-]?(?<chapter>[0-9]+)",
                 // Detect specified parts, like Part 02
                 // 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 is often beginning of filename
-                @"^(?<chapter>\d+)",
+                "^(?<chapter>[0-9]+)",
                 // Part if often ending of filename
                 // Part if often ending of filename
-                @"(?<part>\d+)$",
+                "(?<part>[0-9]+)$",
                 // Sometimes named as 0001_005 (chapter_part)
                 // 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.
                 // 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();
             var extensions = VideoFileExtensions.ToList();
@@ -675,16 +675,16 @@ namespace Emby.Naming.Common
 
 
             MultipleEpisodeExpressions = new string[]
             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)
             }.Select(i => new EpisodeExpression(i)
             {
             {
                 IsNamed = true
                 IsNamed = true

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

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

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

@@ -1111,7 +1111,8 @@ namespace Emby.Server.Implementations.Data
                     continue;
                     continue;
                 }
                 }
 
 
-                str.Append(ToValueString(i) + "|");
+                str.Append(ToValueString(i))
+                    .Append('|');
             }
             }
 
 
             str.Length -= 1; // Remove last |
             str.Length -= 1; // Remove last |
@@ -2472,7 +2473,7 @@ namespace Emby.Server.Implementations.Data
                 var item = query.SimilarTo;
                 var item = query.SimilarTo;
 
 
                 var builder = new StringBuilder();
                 var builder = new StringBuilder();
-                builder.Append("(");
+                builder.Append('(');
 
 
                 if (string.IsNullOrEmpty(item.OfficialRating))
                 if (string.IsNullOrEmpty(item.OfficialRating))
                 {
                 {
@@ -2510,7 +2511,7 @@ namespace Emby.Server.Implementations.Data
             if (!string.IsNullOrEmpty(query.SearchTerm))
             if (!string.IsNullOrEmpty(query.SearchTerm))
             {
             {
                 var builder = new StringBuilder();
                 var builder = new StringBuilder();
-                builder.Append("(");
+                builder.Append('(');
 
 
                 builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)");
                 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)
                 if (i > 0)
                 {
                 {
-                    insertText.Append(",");
+                    insertText.Append(',');
                 }
                 }
 
 
                 insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture));
                 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))
                     foreach (var column in _mediaAttachmentSaveColumns.Skip(1))
                     {
                     {
-                        insertText.Append("@" + column + index + ",");
+                        insertText.Append('@')
+                            .Append(column)
+                            .Append(index)
+                            .Append(',');
                     }
                     }
 
 
                     insertText.Length -= 1;
                     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.ResponseCompression" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
     <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.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="Mono.Nat" Version="2.0.1" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
     <PackageReference Include="ServiceStack.Text.Core" Version="5.9.0" />
     <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;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Emby.Server.Implementations.Udp;
 using Emby.Server.Implementations.Udp;
@@ -48,8 +49,16 @@ namespace Emby.Server.Implementations.EntryPoints
         /// <inheritdoc />
         /// <inheritdoc />
         public Task RunAsync()
         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;
             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)
                 if (info is FileInfo fileInfo)
                 {
                 {
                     result.Length = fileInfo.Length;
                     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;
                     result.DirectoryName = fileInfo.DirectoryName;
                 }
                 }
 
 

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

@@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library
                 if (parent != null)
                 if (parent != null)
                 {
                 {
                     // Don't resolve these into audio files
                     // 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))
                         && _libraryManager.IsAudioFile(filename))
                     {
                     {
                         return true;
                         return true;

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

@@ -11,6 +11,17 @@ namespace Emby.Server.Implementations.Library
 {
 {
     public class ExclusiveLiveStream : ILiveStream
     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 int ConsumerCount { get; set; }
 
 
         public string OriginalStreamId { get; set; }
         public string OriginalStreamId { get; set; }
@@ -21,18 +32,7 @@ namespace Emby.Server.Implementations.Library
 
 
         public MediaSourceInfo MediaSource { get; set; }
         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()
         public Task Close()
         {
         {

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

@@ -59,6 +59,8 @@ namespace Emby.Server.Implementations.Library
     /// </summary>
     /// </summary>
     public class LibraryManager : ILibraryManager
     public class LibraryManager : ILibraryManager
     {
     {
+        private const string ShortcutFileExtension = ".mblink";
+
         private readonly ILogger<LibraryManager> _logger;
         private readonly ILogger<LibraryManager> _logger;
         private readonly ITaskManager _taskManager;
         private readonly ITaskManager _taskManager;
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
@@ -74,63 +76,24 @@ namespace Emby.Server.Implementations.Library
         private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
         private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
         private readonly IImageProcessor _imageProcessor;
         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>
         /// <summary>
-        /// Gets or sets the comparers.
+        /// The _root folder sync lock.
         /// </summary>
         /// </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>
         /// <summary>
-        /// Occurs when [item removed].
+        /// The _root folder.
         /// </summary>
         /// </summary>
-        public event EventHandler<ItemChangeEventArgs> ItemRemoved;
+        private volatile AggregateFolder _rootFolder;
+        private volatile UserRootFolder _userRootFolder;
 
 
-        public bool IsScanRunning { get; private set; }
+        private bool _wizardCompleted;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="LibraryManager" /> class.
         /// Initializes a new instance of the <see cref="LibraryManager" /> class.
@@ -185,37 +148,19 @@ namespace Emby.Server.Implementations.Library
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Adds the parts.
+        /// Occurs when [item added].
         /// </summary>
         /// </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>
         /// <summary>
-        /// The _root folder.
+        /// Occurs when [item updated].
         /// </summary>
         /// </summary>
-        private volatile AggregateFolder _rootFolder;
+        public event EventHandler<ItemChangeEventArgs> ItemUpdated;
 
 
         /// <summary>
         /// <summary>
-        /// The _root folder sync lock.
+        /// Occurs when [item removed].
         /// </summary>
         /// </summary>
-        private readonly object _rootFolderSyncLock = new object();
+        public event EventHandler<ItemChangeEventArgs> ItemRemoved;
 
 
         /// <summary>
         /// <summary>
         /// Gets the root folder.
         /// 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>
         /// <summary>
         /// Records the configuration values.
         /// Records the configuration values.
@@ -340,7 +346,7 @@ namespace Emby.Server.Implementations.Library
             if (item is LiveTvProgram)
             if (item is LiveTvProgram)
             {
             {
                 _logger.LogDebug(
                 _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.GetType().Name,
                     item.Name ?? "Unknown name",
                     item.Name ?? "Unknown name",
                     item.Path ?? string.Empty,
                     item.Path ?? string.Empty,
@@ -349,7 +355,7 @@ namespace Emby.Server.Implementations.Library
             else
             else
             {
             {
                 _logger.LogInformation(
                 _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.GetType().Name,
                     item.Name ?? "Unknown name",
                     item.Name ?? "Unknown name",
                     item.Path ?? string.Empty,
                     item.Path ?? string.Empty,
@@ -367,7 +373,12 @@ namespace Emby.Server.Implementations.Library
                     continue;
                     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
                 try
                 {
                 {
@@ -391,7 +402,13 @@ namespace Emby.Server.Implementations.Library
                     {
                     {
                         try
                         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)
                             if (fileSystemInfo.IsDirectory)
                             {
                             {
                                 Directory.Delete(fileSystemInfo.FullName, true);
                                 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
                 // Try to normalize paths located underneath program-data in an attempt to make them more portable
                 key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length)
                 key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length)
                     .TrimStart(new[] { '/', '\\' })
                     .TrimStart(new[] { '/', '\\' })
-                    .Replace("/", "\\");
+                    .Replace('/', '\\');
             }
             }
 
 
             if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
             if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
@@ -763,14 +780,11 @@ namespace Emby.Server.Implementations.Library
             return rootFolder;
             return rootFolder;
         }
         }
 
 
-        private volatile UserRootFolder _userRootFolder;
-        private readonly object _syncLock = new object();
-
         public Folder GetUserRootFolder()
         public Folder GetUserRootFolder()
         {
         {
             if (_userRootFolder == null)
             if (_userRootFolder == null)
             {
             {
-                lock (_syncLock)
+                lock (_userRootFolderSyncLock)
                 {
                 {
                     if (_userRootFolder == null)
                     if (_userRootFolder == null)
                     {
                     {
@@ -1320,7 +1334,7 @@ namespace Emby.Server.Implementations.Library
 
 
             return new QueryResult<BaseItem>
             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);
                 return _itemRepository.GetItems(query);
             }
             }
 
 
-            var list = _itemRepository.GetItemList(query);
-
             return new QueryResult<BaseItem>
             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();
             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);
                 RegisterItem(item);
                 return;
                 return;
@@ -1933,12 +1946,9 @@ namespace Emby.Server.Implementations.Library
         /// <summary>
         /// <summary>
         /// Updates the item.
         /// Updates the item.
         /// </summary>
         /// </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)
                 if (item.IsFileProtocol)
                 {
                 {
@@ -1950,11 +1960,11 @@ namespace Emby.Server.Implementations.Library
                 UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate);
                 UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate);
             }
             }
 
 
-            _itemRepository.SaveItems(itemsList, cancellationToken);
+            _itemRepository.SaveItems(items, cancellationToken);
 
 
             if (ItemUpdated != null)
             if (ItemUpdated != null)
             {
             {
-                foreach (var item in itemsList)
+                foreach (var item in items)
                 {
                 {
                     // With the live tv guide this just creates too much noise
                     // With the live tv guide this just creates too much noise
                     if (item.SourceType != SourceType.Library)
                     if (item.SourceType != SourceType.Library)
@@ -2177,8 +2187,6 @@ namespace Emby.Server.Implementations.Library
                 .FirstOrDefault(i => !string.IsNullOrEmpty(i));
                 .FirstOrDefault(i => !string.IsNullOrEmpty(i));
         }
         }
 
 
-        private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
-
         public UserView GetNamedView(
         public UserView GetNamedView(
             User user,
             User user,
             string name,
             string name,
@@ -2476,14 +2484,9 @@ namespace Emby.Server.Implementations.Library
 
 
             var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
             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
             try
             {
             {
@@ -2491,11 +2494,13 @@ namespace Emby.Server.Implementations.Library
                 if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase))
                 if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase))
                 {
                 {
                     // Read from metadata
                     // 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)
                     if (mediaInfo.ParentIndexNumber > 0)
                     {
                     {
                         episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
                         episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
@@ -2653,7 +2658,7 @@ namespace Emby.Server.Implementations.Library
 
 
             var videos = videoListResolver.Resolve(fileSystemChildren);
             var videos = videoListResolver.Resolve(fileSystemChildren);
 
 
-            var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase));
+            var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
 
 
             if (currentVideo != null)
             if (currentVideo != null)
             {
             {
@@ -2670,9 +2675,7 @@ namespace Emby.Server.Implementations.Library
                 .Select(video =>
                 .Select(video =>
                 {
                 {
                     // Try to retrieve it from the db. If we don't find it, use the resolved version
                     // 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;
                         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)
         public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
         {
         {
             AddMediaPathInternal(virtualFolderName, pathInfo, true);
             AddMediaPathInternal(virtualFolderName, pathInfo, true);
@@ -3040,11 +3026,6 @@ namespace Emby.Server.Implementations.Library
                 throw new FileNotFoundException("The path does not exist.");
                 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 rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
             var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
             var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 
 
@@ -3083,11 +3064,6 @@ namespace Emby.Server.Implementations.Library
                 throw new ArgumentNullException(nameof(pathInfo));
                 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 rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
             var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
             var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 
 
@@ -3219,7 +3195,8 @@ namespace Emby.Server.Implementations.Library
 
 
             if (!Directory.Exists(virtualFolderPath))
             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)
             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 IMediaEncoder _mediaEncoder;
         private readonly ILogger _logger;
         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)
         public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IJsonSerializer json, IApplicationPaths appPaths)
         {
         {
@@ -72,13 +71,14 @@ namespace Emby.Server.Implementations.Library
 
 
                 mediaSource.AnalyzeDurationMs = 3000;
                 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)
                 if (cacheFilePath != null)
                 {
                 {
@@ -126,7 +126,7 @@ namespace Emby.Server.Implementations.Library
                 mediaSource.RunTimeTicks = null;
                 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)
             if (audioStream == null || audioStream.Index == -1)
             {
             {
@@ -137,7 +137,7 @@ namespace Emby.Server.Implementations.Library
                 mediaSource.DefaultAudioStreamIndex = audioStream.Index;
                 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 != null)
             {
             {
                 if (!videoStream.BitRate.HasValue)
                 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
     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 IItemRepository _itemRepo;
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
@@ -40,6 +43,11 @@ namespace Emby.Server.Implementations.Library
         private readonly ILocalizationManager _localizationManager;
         private readonly ILocalizationManager _localizationManager;
         private readonly IApplicationPaths _appPaths;
         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;
         private IMediaSourceProvider[] _providers;
 
 
         public MediaSourceManager(
         public MediaSourceManager(
@@ -368,7 +376,6 @@ namespace Emby.Server.Implementations.Library
                 }
                 }
             }
             }
 
 
-
             var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
             var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
                 ? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
                 ? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
 
 
@@ -451,9 +458,6 @@ namespace Emby.Server.Implementations.Library
             .ToList();
             .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)
         public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
         {
         {
             await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
             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)
         private Tuple<IMediaSourceProvider, string> GetProvider(string key)
         {
         {
             if (string.IsNullOrEmpty(key))
             if (string.IsNullOrEmpty(key))
@@ -881,9 +882,9 @@ namespace Emby.Server.Implementations.Library
         public void Dispose()
         public void Dispose()
         {
         {
             Dispose(true);
             Dispose(true);
+            GC.SuppressFinalize(this);
         }
         }
 
 
-        private readonly object _disposeLock = new object();
         /// <summary>
         /// <summary>
         /// Releases unmanaged and - optionally - managed resources.
         /// Releases unmanaged and - optionally - managed resources.
         /// </summary>
         /// </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
             // 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)
             if (stream != null)
             {
             {

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

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

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

@@ -4,6 +4,7 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
+using System.Text.Json;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Emby.Server.Implementations.Library;
 using Emby.Server.Implementations.Library;
@@ -28,7 +29,6 @@ using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
@@ -54,7 +54,6 @@ namespace Emby.Server.Implementations.LiveTv
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
         private readonly ITaskManager _taskManager;
         private readonly ITaskManager _taskManager;
         private readonly ILocalizationManager _localization;
         private readonly ILocalizationManager _localization;
-        private readonly IJsonSerializer _jsonSerializer;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly IChannelManager _channelManager;
         private readonly IChannelManager _channelManager;
         private readonly LiveTvDtoService _tvDtoService;
         private readonly LiveTvDtoService _tvDtoService;
@@ -73,7 +72,6 @@ namespace Emby.Server.Implementations.LiveTv
             ILibraryManager libraryManager,
             ILibraryManager libraryManager,
             ITaskManager taskManager,
             ITaskManager taskManager,
             ILocalizationManager localization,
             ILocalizationManager localization,
-            IJsonSerializer jsonSerializer,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
             IChannelManager channelManager,
             IChannelManager channelManager,
             LiveTvDtoService liveTvDtoService)
             LiveTvDtoService liveTvDtoService)
@@ -85,7 +83,6 @@ namespace Emby.Server.Implementations.LiveTv
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
             _taskManager = taskManager;
             _taskManager = taskManager;
             _localization = localization;
             _localization = localization;
-            _jsonSerializer = jsonSerializer;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
             _dtoService = dtoService;
             _dtoService = dtoService;
             _userDataManager = userDataManager;
             _userDataManager = userDataManager;
@@ -2234,7 +2231,7 @@ namespace Emby.Server.Implementations.LiveTv
 
 
         public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)
         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));
             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
             // Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider
             // ServerConfiguration.SaveConfiguration crashes during xml serialization for 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));
             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}",
     "AppDeviceValues": "App: {0}, Gerät: {1}",
     "Application": "Anwendung",
     "Application": "Anwendung",
     "Artists": "Interpreten",
     "Artists": "Interpreten",
-    "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifiziert",
+    "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich angemeldet",
     "Books": "Bücher",
     "Books": "Bücher",
     "CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
     "CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
     "Channels": "Kanäle",
     "Channels": "Kanäle",

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

@@ -5,47 +5,47 @@
     "Artists": "Artis",
     "Artists": "Artis",
     "AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
     "AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
     "Books": "Buku-buku",
     "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",
     "Channels": "Saluran",
-    "ChapterNameValue": "Chapter {0}",
+    "ChapterNameValue": "Bab {0}",
     "Collections": "Koleksi",
     "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}",
     "FailedLoginAttemptWithUserName": "Cubaan log masuk gagal dari {0}",
-    "Favorites": "Favorites",
-    "Folders": "Folders",
+    "Favorites": "Kegemaran",
+    "Folders": "Fail-fail",
     "Genres": "Genre-genre",
     "Genres": "Genre-genre",
-    "HeaderAlbumArtists": "Album Artists",
+    "HeaderAlbumArtists": "Album Artis-artis",
     "HeaderCameraUploads": "Muatnaik Kamera",
     "HeaderCameraUploads": "Muatnaik Kamera",
     "HeaderContinueWatching": "Terus Menonton",
     "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}",
     "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",
     "Music": "Muzik",
     "MusicVideos": "Video 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",
     "NotificationOptionApplicationUpdateInstalled": "Application update installed",
     "NotificationOptionAudioPlayback": "Audio playback started",
     "NotificationOptionAudioPlayback": "Audio playback started",
     "NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
     "NotificationOptionAudioPlaybackStopped": "Audio playback stopped",

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

@@ -67,5 +67,7 @@
     "Artists": "นักแสดง",
     "Artists": "นักแสดง",
     "Application": "แอปพลิเคชั่น",
     "Application": "แอปพลิเคชั่น",
     "AppDeviceValues": "App: {0}, อุปกรณ์: {1}",
     "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;
                 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) ||
             if ((octet[0] == 10) ||
                 (octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918
                 (octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918
@@ -268,6 +273,12 @@ namespace Emby.Server.Implementations.Networking
             string excludeAddress = "[" + addressString + "]";
             string excludeAddress = "[" + addressString + "]";
             var subnets = LocalSubnetsFn();
             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 [ ]
             // Exclude any addresses if they appear in the LAN list in [ ]
             if (Array.IndexOf(subnets, excludeAddress) != -1)
             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());
             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;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Emby.Server.Implementations.HttpServer;
 using Emby.Server.Implementations.HttpServer;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Model.Services;
 using MediaBrowser.Model.Services;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
@@ -78,7 +79,8 @@ namespace Emby.Server.Implementations.Services
             var request = await CreateRequest(httpHost, httpReq, _restPath, logger).ConfigureAwait(false);
             var request = await CreateRequest(httpHost, httpReq, _restPath, logger).ConfigureAwait(false);
 
 
             httpHost.ApplyRequestFilters(httpReq, httpRes, request);
             httpHost.ApplyRequestFilters(httpReq, httpRes, request);
-
+            
+            httpRes.HttpContext.SetServiceStackRequest(httpReq);
             var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
             var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
 
 
             // Apply response filters
             // Apply response filters

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

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

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

@@ -117,23 +117,20 @@ namespace Emby.Server.Implementations.TV
                 limit = limit.Value + 10;
                 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
             // Avoid implicitly captured closure
             var episodes = GetNextUpEpisodes(request, user, items, dtoOptions);
             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);
                 _logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
                 return Array.Empty<PackageInfo>();
                 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 />
         /// <inheritdoc />

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

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

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

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

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

@@ -19,8 +19,8 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <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>
   </ItemGroup>
 
 
 </Project>
 </Project>

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

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

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

@@ -1,5 +1,6 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
+using System;
 using System.Linq;
 using System.Linq;
 using Jellyfin.Data;
 using Jellyfin.Data;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
@@ -135,6 +136,18 @@ namespace Jellyfin.Server.Implementations
             return base.SaveChanges();
             return base.SaveChanges();
         }
         }
 
 
+        /// <inheritdoc/>
+        public override void Dispose()
+        {
+            foreach (var entry in ChangeTracker.Entries())
+            {
+                entry.State = EntityState.Detached;
+            }
+
+            GC.SuppressFinalize(this);
+            base.Dispose();
+        }
+
         /// <inheritdoc />
         /// <inheritdoc />
         protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
         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.Security.Cryptography;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
+using MediaBrowser.Common;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
@@ -23,7 +24,7 @@ namespace Jellyfin.Server.Implementations.Users
         private const string BaseResetFileName = "passwordreset";
         private const string BaseResetFileName = "passwordreset";
 
 
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IJsonSerializer _jsonSerializer;
-        private readonly IUserManager _userManager;
+        private readonly IApplicationHost _appHost;
 
 
         private readonly string _passwordResetFileBase;
         private readonly string _passwordResetFileBase;
         private readonly string _passwordResetFileBaseDir;
         private readonly string _passwordResetFileBaseDir;
@@ -33,16 +34,17 @@ namespace Jellyfin.Server.Implementations.Users
         /// </summary>
         /// </summary>
         /// <param name="configurationManager">The configuration manager.</param>
         /// <param name="configurationManager">The configuration manager.</param>
         /// <param name="jsonSerializer">The JSON serializer.</param>
         /// <param name="jsonSerializer">The JSON serializer.</param>
-        /// <param name="userManager">The user manager.</param>
+        /// <param name="appHost">The application host.</param>
         public DefaultPasswordResetProvider(
         public DefaultPasswordResetProvider(
             IServerConfigurationManager configurationManager,
             IServerConfigurationManager configurationManager,
             IJsonSerializer jsonSerializer,
             IJsonSerializer jsonSerializer,
-            IUserManager userManager)
+            IApplicationHost appHost)
         {
         {
             _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath;
             _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath;
             _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName);
             _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName);
             _jsonSerializer = jsonSerializer;
             _jsonSerializer = jsonSerializer;
-            _userManager = userManager;
+            _appHost = appHost;
+            // TODO: Remove the circular dependency on UserManager
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
@@ -54,6 +56,7 @@ namespace Jellyfin.Server.Implementations.Users
         /// <inheritdoc />
         /// <inheritdoc />
         public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
         public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
         {
         {
+            var userManager = _appHost.Resolve<IUserManager>();
             var usersReset = new List<string>();
             var usersReset = new List<string>();
             foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
             foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
             {
             {
@@ -72,10 +75,10 @@ namespace Jellyfin.Server.Implementations.Users
                     pin.Replace("-", string.Empty, StringComparison.Ordinal),
                     pin.Replace("-", string.Empty, StringComparison.Ordinal),
                     StringComparison.InvariantCultureIgnoreCase))
                     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");
                         ?? 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);
                     usersReset.Add(resetUser.Username);
                     File.Delete(resetFile);
                     File.Delete(resetFile);
                 }
                 }
@@ -121,7 +124,6 @@ namespace Jellyfin.Server.Implementations.Users
             }
             }
 
 
             user.EasyPassword = pin;
             user.EasyPassword = pin;
-            await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
 
 
             return new ForgotPasswordResult
             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 IApplicationHost _appHost;
         private readonly IImageProcessor _imageProcessor;
         private readonly IImageProcessor _imageProcessor;
         private readonly ILogger<UserManager> _logger;
         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>
         /// <summary>
         /// Initializes a new instance of the <see cref="UserManager"/> class.
         /// Initializes a new instance of the <see cref="UserManager"/> class.
@@ -69,6 +68,13 @@ namespace Jellyfin.Server.Implementations.Users
             _appHost = appHost;
             _appHost = appHost;
             _imageProcessor = imageProcessor;
             _imageProcessor = imageProcessor;
             _logger = logger;
             _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/>
         /// <inheritdoc/>
@@ -102,7 +108,16 @@ namespace Jellyfin.Server.Implementations.Users
         }
         }
 
 
         /// <inheritdoc/>
         /// <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/>
         /// <inheritdoc/>
         public User? GetUserById(Guid id)
         public User? GetUserById(Guid id)
@@ -152,12 +167,12 @@ namespace Jellyfin.Server.Implementations.Users
                 throw new ArgumentException("Invalid username", nameof(newName));
                 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.");
                 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(
                 throw new ArgumentException(string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
@@ -188,8 +203,24 @@ namespace Jellyfin.Server.Implementations.Users
             await dbContext.SaveChangesAsync().ConfigureAwait(false);
             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/>
         /// <inheritdoc/>
-        public User CreateUser(string name)
+        public async Task<User> CreateUserAsync(string name)
         {
         {
             if (!IsValidUsername(name))
             if (!IsValidUsername(name))
             {
             {
@@ -198,18 +229,10 @@ namespace Jellyfin.Server.Implementations.Users
 
 
             using var dbContext = _dbProvider.CreateContext();
             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.Users.Add(newUser);
-            dbContext.SaveChanges();
+            await dbContext.SaveChangesAsync().ConfigureAwait(false);
 
 
             OnUserCreated?.Invoke(this, new GenericEventArgs<User>(newUser));
             OnUserCreated?.Invoke(this, new GenericEventArgs<User>(newUser));
 
 
@@ -512,7 +535,7 @@ namespace Jellyfin.Server.Implementations.Users
             }
             }
             else
             else
             {
             {
-                IncrementInvalidLoginAttemptCount(user);
+                await IncrementInvalidLoginAttemptCount(user).ConfigureAwait(false);
                 _logger.LogInformation(
                 _logger.LogInformation(
                     "Authentication request for {UserName} has been denied (IP: {IP}).",
                     "Authentication request for {UserName} has been denied (IP: {IP}).",
                     user.Username,
                     user.Username,
@@ -530,7 +553,12 @@ namespace Jellyfin.Server.Implementations.Users
             if (user != null && isInNetwork)
             if (user != null && isInNetwork)
             {
             {
                 var passwordResetProvider = GetPasswordResetProvider(user);
                 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
             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 />
         /// <inheritdoc />
-        public void Initialize()
+        public async Task InitializeAsync()
         {
         {
             // TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
             // TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
             using var dbContext = _dbProvider.CreateContext();
             using var dbContext = _dbProvider.CreateContext();
 
 
-            if (dbContext.Users.Any())
+            if (await dbContext.Users.AnyAsync().ConfigureAwait(false))
             {
             {
                 return;
                 return;
             }
             }
@@ -595,13 +612,13 @@ namespace Jellyfin.Server.Implementations.Users
                 throw new ArgumentException("Provided username is not valid!", defaultName);
                 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.IsAdministrator, true);
             newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
             newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
             newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
             newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
 
 
-            dbContext.Users.Update(newUser);
-            dbContext.SaveChanges();
+            dbContext.Users.Add(newUser);
+            await dbContext.SaveChangesAsync().ConfigureAwait(false);
         }
         }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
@@ -637,7 +654,7 @@ namespace Jellyfin.Server.Implementations.Users
         /// <inheritdoc/>
         /// <inheritdoc/>
         public void UpdateConfiguration(Guid userId, UserConfiguration config)
         public void UpdateConfiguration(Guid userId, UserConfiguration config)
         {
         {
-            var dbContext = _dbProvider.CreateContext();
+            using var dbContext = _dbProvider.CreateContext();
             var user = dbContext.Users
             var user = dbContext.Users
                            .Include(u => u.Permissions)
                            .Include(u => u.Permissions)
                            .Include(u => u.Preferences)
                            .Include(u => u.Preferences)
@@ -670,8 +687,14 @@ namespace Jellyfin.Server.Implementations.Users
         /// <inheritdoc/>
         /// <inheritdoc/>
         public void UpdatePolicy(Guid userId, UserPolicy policy)
         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"
             // 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
             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++;
             user.InvalidLoginAttemptCount++;
             int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
             int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
@@ -890,7 +913,7 @@ namespace Jellyfin.Server.Implementations.Users
                     user.InvalidLoginAttemptCount);
                     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="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>
         /// <param name="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param>
         public CoreAppHost(
         public CoreAppHost(
-            ServerApplicationPaths applicationPaths,
+            IServerApplicationPaths applicationPaths,
             ILoggerFactory loggerFactory,
             ILoggerFactory loggerFactory,
-            StartupOptions options,
+            IStartupOptions options,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
             INetworkManager networkManager)
             INetworkManager networkManager)
             : base(
             : base(

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

@@ -41,8 +41,8 @@
 
 
   <ItemGroup>
   <ItemGroup>
     <PackageReference Include="CommandLineParser" Version="2.8.0" />
     <PackageReference Include="CommandLineParser" Version="2.8.0" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.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" Version="3.6.0" />
     <PackageReference Include="prometheus-net.AspNetCore" Version="3.6.0" />
     <PackageReference Include="prometheus-net.AspNetCore" Version="3.6.0" />
     <PackageReference Include="Serilog.AspNetCore" Version="3.2.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>
         /// </summary>
         public string Name { get; }
         public string Name { get; }
 
 
+        /// <summary>
+        /// Gets a value indicating whether to perform migration on a new install.
+        /// </summary>
+        public bool PerformOnNewInstall { get; }
+
         /// <summary>
         /// <summary>
         /// Execute the migration routine.
         /// Execute the migration routine.
         /// </summary>
         /// </summary>

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

@@ -22,6 +22,7 @@ namespace Jellyfin.Server.Migrations
             typeof(Routines.RemoveDuplicateExtras),
             typeof(Routines.RemoveDuplicateExtras),
             typeof(Routines.AddDefaultPluginRepository),
             typeof(Routines.AddDefaultPluginRepository),
             typeof(Routines.MigrateUserDb),
             typeof(Routines.MigrateUserDb),
+            typeof(Routines.ReaddDefaultPluginRepository),
             typeof(Routines.MigrateDisplayPreferencesDb)
             typeof(Routines.MigrateDisplayPreferencesDb)
         };
         };
 
 
@@ -44,9 +45,8 @@ namespace Jellyfin.Server.Migrations
                 // If startup wizard is not finished, this is a fresh install.
                 // If startup wizard is not finished, this is a fresh install.
                 // Don't run any migrations, just mark all of them as applied.
                 // 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");
                 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);
                 host.ServerConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions);
-                return;
             }
             }
 
 
             var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet();
             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/>
         /// <inheritdoc/>
         public string Name => "AddDefaultPluginRepository";
         public string Name => "AddDefaultPluginRepository";
 
 
+        /// <inheritdoc/>
+        public bool PerformOnNewInstall => true;
+
         /// <inheritdoc/>
         /// <inheritdoc/>
         public void Perform()
         public void Perform()
         {
         {

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

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

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

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

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

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

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

@@ -54,6 +54,9 @@ namespace Jellyfin.Server.Migrations.Routines
         /// <inheritdoc/>
         /// <inheritdoc/>
         public string Name => "MigrateUserDatabase";
         public string Name => "MigrateUserDatabase";
 
 
+        /// <inheritdoc/>
+        public bool PerformOnNewInstall => false;
+
         /// <inheritdoc/>
         /// <inheritdoc/>
         public void Perform()
         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/>
         /// <inheritdoc/>
         public string Name => "RemoveDuplicateExtras";
         public string Name => "RemoveDuplicateExtras";
 
 
+        /// <inheritdoc/>
+        public bool PerformOnNewInstall => false;
+
         /// <inheritdoc/>
         /// <inheritdoc/>
         public void Perform()
         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))
                 .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(commandLineOpts, appPaths, startupConfig))
                 .UseSerilog()
                 .UseSerilog()

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

@@ -895,6 +895,11 @@ namespace MediaBrowser.Api.Images
             // Handle image/png; charset=utf-8
             // Handle image/png; charset=utf-8
             mimeType = mimeType.Split(';').FirstOrDefault();
             mimeType = mimeType.Split(';').FirstOrDefault();
             var userDataPath = Path.Combine(ServerConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
             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)));
             user.ProfileImage = new Jellyfin.Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
 
 
             await _providerManager
             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();
                 DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
 
 
             var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>(
             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);
             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>
         /// <returns>System.Object.</returns>
         public async Task<object> Post(CreateUserByName request)
         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
             // no need to authenticate password for new user
             if (request.Password != null)
             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>
 
 
   <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" />
     <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
   </ItemGroup>
   </ItemGroup>
 
 

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

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

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

@@ -13,8 +13,8 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <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>
 
 
   <ItemGroup>
   <ItemGroup>

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

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
+using System.Runtime.InteropServices;
 using System.Text;
 using System.Text;
 using System.Threading;
 using System.Threading;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
@@ -371,7 +372,7 @@ namespace MediaBrowser.Controller.MediaEncoding
         public int GetVideoProfileScore(string profile)
         public int GetVideoProfileScore(string profile)
         {
         {
             // strip spaces because they may be stripped out on the query string
             // 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));
             return Array.FindIndex(_videoProfiles, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase));
         }
         }
 
 
@@ -449,41 +450,59 @@ namespace MediaBrowser.Controller.MediaEncoding
             var arg = new StringBuilder();
             var arg = new StringBuilder();
             var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty;
             var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty;
             var outputVideoCodec = GetVideoEncoder(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)
                         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
                         // While using SW decoder
                         else
                         else
@@ -806,6 +825,34 @@ namespace MediaBrowser.Controller.MediaEncoding
                         break;
                         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
             else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // webm
             {
             {
                 // Values 0-3, 0 being highest quality but slower
                 // Values 0-3, 0 being highest quality but slower
@@ -1351,7 +1398,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 transcoderChannelLimit = 6;
                 transcoderChannelLimit = 6;
             }
             }
 
 
-            var isTranscodingAudio = !EncodingHelper.IsCopyCodec(codec);
+            var isTranscodingAudio = !IsCopyCodec(codec);
 
 
             int? resultChannels = state.GetRequestedAudioChannels(codec);
             int? resultChannels = state.GetRequestedAudioChannels(codec);
             if (isTranscodingAudio)
             if (isTranscodingAudio)
@@ -1555,28 +1602,44 @@ namespace MediaBrowser.Controller.MediaEncoding
                 var index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase);
                 var index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase);
                 if (index != -1)
                 if (index != -1)
                 {
                 {
-                    outputSizeParam = "," + outputSizeParam.Substring(index);
+                    outputSizeParam = outputSizeParam.Substring(index);
                 }
                 }
                 else
                 else
                 {
                 {
-                    index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase);
+                    index = outputSizeParam.IndexOf("hwupload=extra_hw_frames", StringComparison.OrdinalIgnoreCase);
                     if (index != -1)
                     if (index != -1)
                     {
                     {
-                        outputSizeParam = "," + outputSizeParam.Substring(index);
+                        outputSizeParam = outputSizeParam.Substring(index);
                     }
                     }
                     else
                     else
                     {
                     {
-                        index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase);
+                        index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase);
                         if (index != -1)
                         if (index != -1)
                         {
                         {
-                            outputSizeParam = "," + outputSizeParam.Substring(index);
+                            outputSizeParam = outputSizeParam.Substring(index);
                         }
                         }
                         else
                         else
                         {
                         {
-                            index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase);
+                            index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase);
                             if (index != -1)
                             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 videoSizeParam = string.Empty;
             var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options) ?? string.Empty;
             var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options) ?? string.Empty;
+            var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
 
 
             // Setup subtitle scaling
             // Setup subtitle scaling
             if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue)
             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,
                         CultureInfo.InvariantCulture,
-                        "scale={0}:{1}",
+                        "scale={0}x{1}",
                         width.Value,
                         width.Value,
                         height.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;
                 : state.SubtitleStream.Index;
 
 
             // Setup default filtergraph utilizing FFMpeg overlay() and FFMpeg scale() (see the return of this function for index reference)
             // 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
             // When the input may or may not be hardware VAAPI decodable
             if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
             if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
@@ -1644,12 +1697,11 @@ namespace MediaBrowser.Controller.MediaEncoding
                     [sub]: SW scaling subtitle to FixedOutputSize
                     [sub]: SW scaling subtitle to FixedOutputSize
                     [base][sub]: SW overlay
                     [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\"";
                 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
             // 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))
                 && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
             {
             {
                 /*
                 /*
@@ -1657,7 +1709,6 @@ namespace MediaBrowser.Controller.MediaEncoding
                     [sub]: SW scaling subtitle to FixedOutputSize
                     [sub]: SW scaling subtitle to FixedOutputSize
                     [base][sub]: SW overlay
                     [base][sub]: SW overlay
                 */
                 */
-                outputSizeParam = outputSizeParam.TrimStart(',');
                 retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"";
                 retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"";
             }
             }
             else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
             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.
                     QSV in FFMpeg can now setup hardware overlay for transcodes.
                     For software decoding and hardware encoding option, frames must be hwuploaded into hardware
                     For software decoding and hardware encoding option, frames must be hwuploaded into hardware
                     with fixed frame size.
                     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,
                 requestedMaxWidth,
                 requestedMaxHeight);
                 requestedMaxHeight);
 
 
-            var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
-
             if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
             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
                 && width.HasValue
                 && height.HasValue)
                 && height.HasValue)
             {
             {
@@ -1758,6 +1806,10 @@ namespace MediaBrowser.Controller.MediaEncoding
                 var outputWidth = width.Value;
                 var outputWidth = width.Value;
                 var outputHeight = height.Value;
                 var outputHeight = height.Value;
                 var qsv_or_vaapi = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase);
                 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
                 if (!videoWidth.HasValue
                     || outputWidth != videoWidth.Value
                     || outputWidth != videoWidth.Value
@@ -1769,15 +1821,20 @@ namespace MediaBrowser.Controller.MediaEncoding
                     filters.Add(
                     filters.Add(
                         string.Format(
                         string.Format(
                             CultureInfo.InvariantCulture,
                             CultureInfo.InvariantCulture,
-                            "{0}=w={1}:h={2}:format=nv12",
+                            "{0}=w={1}:h={2}:format=nv12{3}",
                             qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi",
                             qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi",
                             outputWidth,
                             outputWidth,
-                            outputHeight));
+                            outputHeight,
+                            (qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty));
                 }
                 }
                 else
                 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
             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/
             // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/
 
 
             var request = state.BaseRequest;
             var request = state.BaseRequest;
-
             var videoStream = state.VideoStream;
             var videoStream = state.VideoStream;
             var filters = new List<string>();
             var filters = new List<string>();
 
 
@@ -1998,32 +2054,34 @@ namespace MediaBrowser.Controller.MediaEncoding
             var inputHeight = videoStream?.Height;
             var inputHeight = videoStream?.Height;
             var threeDFormat = state.MediaSource.Video3DFormat;
             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 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
             // 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("format=nv12|vaapi");
                 filters.Add("hwupload");
                 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
             // 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 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
                 // Assert 10-bit hardware VAAPI decodable
                 if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
                 if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
@@ -2048,49 +2106,49 @@ namespace MediaBrowser.Controller.MediaEncoding
             }
             }
 
 
             // Add hardware deinterlace filter before scaling filter
             // 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"));
                     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
             // 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;
                 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 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)
                 if (string.Equals(options.DeinterlaceMethod, "yadif_bob", StringComparison.OrdinalIgnoreCase) && (inputFramerate ?? 60) <= 30)
                 {
                 {
-                    filters.Add("yadif=1:-1:0");
+                    deintParam = "yadif=1:-1:0";
                 }
                 }
                 else
                 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
             // 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));
             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
                     // Test passed on Intel and AMD gfx
                     filters.Add("hwmap=mode=read+write");
                     filters.Add("hwmap=mode=read+write");
@@ -2100,9 +2158,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 
 
             var output = string.Empty;
             var output = string.Empty;
 
 
-            if (state.SubtitleStream != null
-                && state.SubtitleStream.IsTextSubtitleStream
-                && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+            if (hasTextSubs)
             {
             {
                 var subParam = GetTextSubtitleParam(state);
                 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
                 // Ensure proper filters are passed to ffmpeg in case of hardware acceleration via VA-API
                 // Reference: https://trac.ffmpeg.org/wiki/Hardware/VAAPI
                 // Reference: https://trac.ffmpeg.org/wiki/Hardware/VAAPI
-                if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+                if (isVaapiH264Encoder)
                 {
                 {
                     filters.Add("hwmap");
                     filters.Add("hwmap");
                 }
                 }
@@ -2264,7 +2320,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 flags.Add("+ignidx");
                 flags.Add("+ignidx");
             }
             }
 
 
-            if (state.GenPtsInput || EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+            if (state.GenPtsInput || IsCopyCodec(state.OutputVideoCodec))
             {
             {
                 flags.Add("+genpts");
                 flags.Add("+genpts");
             }
             }
@@ -2290,7 +2346,8 @@ namespace MediaBrowser.Controller.MediaEncoding
             {
             {
                 inputModifier += " " + videoDecoder;
                 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 videoStream = state.VideoStream;
                     var inputWidth = videoStream?.Width;
                     var inputWidth = videoStream?.Width;
@@ -2303,11 +2360,19 @@ namespace MediaBrowser.Controller.MediaEncoding
                         && width.HasValue
                         && width.HasValue
                         && height.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>
         /// <summary>
-        /// Gets the name of the output video codec.
+        /// Gets the ffmpeg option string for the hardware accelerated video decoder.
         /// </summary>
         /// </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)
         protected string GetHardwareAcceleratedVideoDecoder(EncodingJobInfo state, EncodingOptions encodingOptions)
         {
         {
-            var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile;
             var videoStream = state.VideoStream;
             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;
                 return null;
             }
             }
 
 
+            var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile;
             // Only use alternative encoders for video files.
             // 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
             // 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.
             // 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;
                 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.
                 // Only hevc and vp9 formats have 10-bit hardware decoder support now.
                 if (isColorDepth10 && !(string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)
                 if (isColorDepth10 && !(string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)
                     || string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase)
                     || string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase)
@@ -2625,13 +2695,6 @@ namespace MediaBrowser.Controller.MediaEncoding
                         case "h264":
                         case "h264":
                             if (_mediaEncoder.SupportsDecoder("h264_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase))
                             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";
                                 return "-c:v h264_cuvid";
                             }
                             }
 
 
@@ -2898,21 +2961,24 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// </summary>
         /// </summary>
         public string GetHwaccelType(EncodingJobInfo state, EncodingOptions options, string videoCodec)
         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 isWindows8orLater = Environment.OSVersion.Version.Major > 6 || (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor > 1);
             var isDxvaSupported = _mediaEncoder.SupportsHwaccel("dxva2") || _mediaEncoder.SupportsHwaccel("d3d11va");
             var isDxvaSupported = _mediaEncoder.SupportsHwaccel("dxva2") || _mediaEncoder.SupportsHwaccel("d3d11va");
 
 
             if ((isDxvaSupported || IsVaapiSupported(state)) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase))
             if ((isDxvaSupported || IsVaapiSupported(state)) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase))
             {
             {
-                if (!isWindows)
+                if (isLinux)
                 {
                 {
                     return "-hwaccel vaapi";
                     return "-hwaccel vaapi";
                 }
                 }
-                else if (isWindows8orLater)
+
+                if (isWindows && isWindows8orLater)
                 {
                 {
                     return "-hwaccel d3d11va";
                     return "-hwaccel d3d11va";
                 }
                 }
-                else
+
+                if (isWindows && !isWindows8orLater)
                 {
                 {
                     return "-hwaccel dxva2";
                     return "-hwaccel dxva2";
                 }
                 }
@@ -3002,7 +3068,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 args += " -mpegts_m2ts_mode 1";
                 args += " -mpegts_m2ts_mode 1";
             }
             }
 
 
-            if (EncodingHelper.IsCopyCodec(videoCodec))
+            if (IsCopyCodec(videoCodec))
             {
             {
                 if (state.VideoStream != null
                 if (state.VideoStream != null
                     && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase)
                     && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase)
@@ -3104,7 +3170,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 
 
             var args = "-codec:a:0 " + codec;
             var args = "-codec:a:0 " + codec;
 
 
-            if (EncodingHelper.IsCopyCodec(codec))
+            if (IsCopyCodec(codec))
             {
             {
                 return args;
                 return args;
             }
             }
@@ -3181,5 +3247,42 @@ namespace MediaBrowser.Controller.MediaEncoding
         {
         {
             return string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase);
             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)
         internal static Version GetFFmpegVersion(string output)
         {
         {
             // For pre-built binaries the FFmpeg version should be mentioned at the very start of the 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)
             if (match.Success)
             {
             {
@@ -225,7 +225,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             var rc = new StringBuilder(144);
             var rc = new StringBuilder(144);
             foreach (Match m in Regex.Matches(
             foreach (Match m in Regex.Matches(
                 output,
                 output,
-                @"((?<name>lib\w+)\s+(?<major>\d+)\.\s*(?<minor>\d+))",
+                @"((?<name>lib\w+)\s+(?<major>[0-9]+)\.\s*(?<minor>[0-9]+))",
                 RegexOptions.Multiline))
                 RegexOptions.Multiline))
             {
             {
                 rc.Append(m.Groups["name"])
                 rc.Append(m.Groups["name"])

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

@@ -172,7 +172,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                 inputFiles = new[] { mediaSource.Path };
                 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);
             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>
   <ItemGroup>
     <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
     <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.Globalization" Version="4.3.0" />
     <PackageReference Include="System.Text.Json" Version="4.7.2" />
     <PackageReference Include="System.Text.Json" Version="4.7.2" />
   </ItemGroup>
   </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
 #pragma warning disable CS1591
 
 
 using System;
 using System;
@@ -9,21 +10,27 @@ namespace MediaBrowser.Model.Notifications
         public NotificationOption(string type)
         public NotificationOption(string type)
         {
         {
             Type = type;
             Type = type;
+            DisabledServices = Array.Empty<string>();
+            DisabledMonitorUsers = Array.Empty<string>();
+            SendToUsers = Array.Empty<string>();
+        }
 
 
+        public NotificationOption()
+        {
             DisabledServices = Array.Empty<string>();
             DisabledServices = Array.Empty<string>();
             DisabledMonitorUsers = Array.Empty<string>();
             DisabledMonitorUsers = Array.Empty<string>();
             SendToUsers = Array.Empty<string>();
             SendToUsers = Array.Empty<string>();
         }
         }
 
 
-        public string Type { get; set; }
+        public string? Type { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// User Ids to not monitor (it's opt out).
+        /// Gets or sets user Ids to not monitor (it's opt out).
         /// </summary>
         /// </summary>
         public string[] DisabledMonitorUsers { get; set; }
         public string[] DisabledMonitorUsers { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// User Ids to send to (if SendToUserMode == Custom)
+        /// Gets or sets user Ids to send to (if SendToUserMode == Custom).
         /// </summary>
         /// </summary>
         public string[] SendToUsers { get; set; }
         public string[] SendToUsers { get; set; }
 
 

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

@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
 using System;
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;
@@ -25,7 +23,6 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Events;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using Priority_Queue;
 using Priority_Queue;
 using Book = MediaBrowser.Controller.Entities.Book;
 using Book = MediaBrowser.Controller.Entities.Book;
@@ -42,33 +39,38 @@ namespace MediaBrowser.Providers.Manager
     /// </summary>
     /// </summary>
     public class ProviderManager : IProviderManager, IDisposable
     public class ProviderManager : IProviderManager, IDisposable
     {
     {
+        private readonly object _refreshQueueLock = new object();
         private readonly ILogger<ProviderManager> _logger;
         private readonly ILogger<ProviderManager> _logger;
         private readonly IHttpClient _httpClient;
         private readonly IHttpClient _httpClient;
         private readonly ILibraryMonitor _libraryMonitor;
         private readonly ILibraryMonitor _libraryMonitor;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly IServerApplicationPaths _appPaths;
         private readonly IServerApplicationPaths _appPaths;
-        private readonly IJsonSerializer _json;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
         private readonly ISubtitleManager _subtitleManager;
         private readonly ISubtitleManager _subtitleManager;
         private readonly IServerConfigurationManager _configurationManager;
         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 IEnumerable<IMetadataSaver> _savers;
-
         private IExternalId[] _externalIds;
         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>
         /// <summary>
-        /// Initializes a new instance of the <see cref="ProviderManager" /> class.
+        /// Initializes a new instance of the <see cref="ProviderManager"/> class.
         /// </summary>
         /// </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(
         public ProviderManager(
             IHttpClient httpClient,
             IHttpClient httpClient,
             ISubtitleManager subtitleManager,
             ISubtitleManager subtitleManager,
@@ -77,8 +79,7 @@ namespace MediaBrowser.Providers.Manager
             ILogger<ProviderManager> logger,
             ILogger<ProviderManager> logger,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
             IServerApplicationPaths appPaths,
             IServerApplicationPaths appPaths,
-            ILibraryManager libraryManager,
-            IJsonSerializer json)
+            ILibraryManager libraryManager)
         {
         {
             _logger = logger;
             _logger = logger;
             _httpClient = httpClient;
             _httpClient = httpClient;
@@ -87,16 +88,27 @@ namespace MediaBrowser.Providers.Manager
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
             _appPaths = appPaths;
             _appPaths = appPaths;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
-            _json = json;
             _subtitleManager = subtitleManager;
             _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();
             ImageProviders = imageProviders.ToArray();
 
 
@@ -104,27 +116,17 @@ namespace MediaBrowser.Providers.Manager
             _metadataProviders = metadataProviders.ToArray();
             _metadataProviders = metadataProviders.ToArray();
             _externalIds = externalIds.OrderBy(i => i.ProviderName).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)
         public Task<ItemUpdateType> RefreshSingleItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
         {
-            IMetadataService service = null;
             var type = item.GetType();
             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)
             if (service == null)
             {
             {
@@ -147,35 +149,36 @@ namespace MediaBrowser.Providers.Manager
             return Task.FromResult(ItemUpdateType.None);
             return Task.FromResult(ItemUpdateType.None);
         }
         }
 
 
+        /// <inheritdoc/>
         public async Task SaveImage(BaseItem item, string url, ImageType type, int? imageIndex, CancellationToken cancellationToken)
         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,
                 CancellationToken = cancellationToken,
                 Url = url,
                 Url = url,
                 BufferContent = false
                 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)
         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);
             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)
         public Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken)
         {
         {
             if (string.IsNullOrWhiteSpace(source))
             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);
             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)
         public Task SaveImage(User user, Stream source, string mimeType, string path)
         {
         {
             return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger)
             return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger)
                 .SaveImage(user, source, path);
                 .SaveImage(user, source, path);
         }
         }
 
 
+        /// <inheritdoc/>
         public async Task<IEnumerable<RemoteImageInfo>> GetAvailableRemoteImages(BaseItem item, RemoteImageQuery query, CancellationToken cancellationToken)
         public async Task<IEnumerable<RemoteImageInfo>> GetAvailableRemoteImages(BaseItem item, RemoteImageQuery query, CancellationToken cancellationToken)
         {
         {
             var providers = GetRemoteImageProviders(item, query.IncludeDisabledProviders);
             var providers = GetRemoteImageProviders(item, query.IncludeDisabledProviders);
@@ -213,7 +218,7 @@ namespace MediaBrowser.Providers.Manager
                 languages.Add(preferredLanguage);
                 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);
             var results = await Task.WhenAll(tasks).ConfigureAwait(false);
 
 
@@ -224,12 +229,17 @@ namespace MediaBrowser.Providers.Manager
         /// Gets the images.
         /// Gets the images.
         /// </summary>
         /// </summary>
         /// <param name="item">The item.</param>
         /// <param name="item">The item.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="provider">The provider.</param>
         /// <param name="provider">The provider.</param>
         /// <param name="preferredLanguages">The preferred languages.</param>
         /// <param name="preferredLanguages">The preferred languages.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="type">The type.</param>
         /// <param name="type">The type.</param>
         /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
         /// <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
             try
             {
             {
@@ -255,21 +265,23 @@ namespace MediaBrowser.Providers.Manager
             }
             }
             catch (Exception ex)
             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>();
                 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)
         public IEnumerable<ImageProviderInfo> GetRemoteImageProviderInfo(BaseItem item)
         {
         {
             return GetRemoteImageProviders(item, true).Select(i => new ImageProviderInfo(i.Name, i.GetSupportedImages(item).ToArray()));
             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)
         public IEnumerable<IImageProvider> GetImageProviders(BaseItem item, ImageRefreshOptions refreshOptions)
         {
         {
             return GetImageProviders(item, _libraryManager.GetLibraryOptions(item), GetMetadataOptions(item), refreshOptions, false);
             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 typeOptions = libraryOptions.GetTypeOptions(item.GetType().Name);
             var typeFetcherOrder = typeOptions?.ImageFetcherOrder;
             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 =>
                 .OrderBy(i =>
                 {
                 {
                     // See if there's a user-defined order
                     // See if there's a user-defined order
@@ -304,6 +316,13 @@ namespace MediaBrowser.Providers.Manager
             .ThenBy(GetOrder);
             .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)
         public IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions)
             where T : BaseItem
             where T : BaseItem
         {
         {
@@ -319,7 +338,7 @@ namespace MediaBrowser.Providers.Manager
             var currentOptions = globalMetadataOptions;
             var currentOptions = globalMetadataOptions;
 
 
             return _metadataProviders.OfType<IMetadataProvider<T>>()
             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))
                 .OrderBy(i => GetConfiguredOrder(item, i, libraryOptions, globalMetadataOptions))
                 .ThenBy(GetDefaultOrder);
                 .ThenBy(GetDefaultOrder);
         }
         }
@@ -329,14 +348,20 @@ namespace MediaBrowser.Providers.Manager
             var options = GetMetadataOptions(item);
             var options = GetMetadataOptions(item);
             var libraryOptions = _libraryManager.GetLibraryOptions(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)
             if (!includeDisabled)
             {
             {
@@ -372,7 +397,12 @@ namespace MediaBrowser.Providers.Manager
             return true;
             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)
             if (!includeDisabled)
             {
             {
@@ -400,7 +430,7 @@ namespace MediaBrowser.Providers.Manager
             }
             }
             catch (Exception ex)
             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;
                 return false;
             }
             }
         }
         }
@@ -412,9 +442,7 @@ namespace MediaBrowser.Providers.Manager
         /// <returns>System.Int32.</returns>
         /// <returns>System.Int32.</returns>
         private int GetOrder(IImageProvider provider)
         private int GetOrder(IImageProvider provider)
         {
         {
-            var hasOrder = provider as IHasOrder;
-
-            if (hasOrder == null)
+            if (!(provider is IHasOrder hasOrder))
             {
             {
                 return 0;
                 return 0;
             }
             }
@@ -441,7 +469,7 @@ namespace MediaBrowser.Providers.Manager
             if (provider is IRemoteMetadataProvider)
             if (provider is IRemoteMetadataProvider)
             {
             {
                 var typeOptions = libraryOptions.GetTypeOptions(item.GetType().Name);
                 var typeOptions = libraryOptions.GetTypeOptions(item.GetType().Name);
-                var typeFetcherOrder = typeOptions == null ? null : typeOptions.MetadataFetcherOrder;
+                var typeFetcherOrder = typeOptions?.MetadataFetcherOrder;
 
 
                 var fetcherOrder = typeFetcherOrder ?? globalMetadataOptions.MetadataFetcherOrder;
                 var fetcherOrder = typeFetcherOrder ?? globalMetadataOptions.MetadataFetcherOrder;
 
 
@@ -459,9 +487,7 @@ namespace MediaBrowser.Providers.Manager
 
 
         private int GetDefaultOrder(IMetadataProvider provider)
         private int GetDefaultOrder(IMetadataProvider provider)
         {
         {
-            var hasOrder = provider as IHasOrder;
-
-            if (hasOrder != null)
+            if (provider is IHasOrder hasOrder)
             {
             {
                 return hasOrder.Order;
                 return hasOrder.Order;
             }
             }
@@ -469,9 +495,10 @@ namespace MediaBrowser.Providers.Manager
             return 0;
             return 0;
         }
         }
 
 
+        /// <inheritdoc/>
         public MetadataPluginSummary[] GetAllMetadataPlugins()
         public MetadataPluginSummary[] GetAllMetadataPlugins()
         {
         {
-            return new MetadataPluginSummary[]
+            return new[]
             {
             {
                 GetPluginSummary<Movie>(),
                 GetPluginSummary<Movie>(),
                 GetPluginSummary<BoxSet>(),
                 GetPluginSummary<BoxSet>(),
@@ -493,7 +520,7 @@ namespace MediaBrowser.Providers.Manager
             where T : BaseItem, new()
             where T : BaseItem, new()
         {
         {
             // Give it a dummy path just so that it looks like a file system item
             // 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"),
                 Path = Path.Combine(_appPaths.InternalMetadataPath, "dummy"),
                 ParentId = Guid.NewGuid()
                 ParentId = Guid.NewGuid()
@@ -508,11 +535,12 @@ namespace MediaBrowser.Providers.Manager
 
 
             var libraryOptions = new LibraryOptions();
             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();
             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)
         private void AddImagePlugins<T>(List<MetadataPlugin> list, T item, List<IImageProvider> imageProviders)
             where T : BaseItem
             where T : BaseItem
         {
         {
-
             // Locals
             // Locals
             list.AddRange(imageProviders.Where(i => (i is ILocalImageProvider)).Select(i => new MetadataPlugin
             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)
         public MetadataOptions GetMetadataOptions(BaseItem item)
         {
         {
             var type = item.GetType().Name;
             var type = item.GetType().Name;
@@ -597,17 +625,13 @@ namespace MediaBrowser.Providers.Manager
                 new MetadataOptions();
                 new MetadataOptions();
         }
         }
 
 
-        /// <summary>
-        /// Saves the metadata.
-        /// </summary>
+        /// <inheritdoc/>
         public void SaveMetadata(BaseItem item, ItemUpdateType updateType)
         public void SaveMetadata(BaseItem item, ItemUpdateType updateType)
         {
         {
             SaveMetadata(item, updateType, _savers);
             SaveMetadata(item, updateType, _savers);
         }
         }
 
 
-        /// <summary>
-        /// Saves the metadata.
-        /// </summary>
+        /// <inheritdoc/>
         public void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<string> savers)
         public void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<string> savers)
         {
         {
             SaveMetadata(item, updateType, _savers.Where(i => savers.Contains(i.Name, StringComparer.OrdinalIgnoreCase)));
             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="item">The item.</param>
         /// <param name="updateType">Type of the update.</param>
         /// <param name="updateType">Type of the update.</param>
         /// <param name="savers">The savers.</param>
         /// <param name="savers">The savers.</param>
-        /// <returns>Task.</returns>
         private void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<IMetadataSaver> savers)
         private void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<IMetadataSaver> savers)
         {
         {
             var libraryOptions = _libraryManager.GetLibraryOptions(item);
             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);
                 _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
                     try
                     {
                     {
@@ -699,11 +720,9 @@ namespace MediaBrowser.Providers.Manager
                         {
                         {
                             if (updateType >= ItemUpdateType.MetadataEdit)
                             if (updateType >= ItemUpdateType.MetadataEdit)
                             {
                             {
-                                var fileSaver = saver as IMetadataFileSaver;
-
                                 // Manual edit occurred
                                 // Manual edit occurred
                                 // Even if save local is off, save locally anyway if the metadata file already exists
                                 // 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;
                                     return false;
                                 }
                                 }
@@ -734,6 +753,7 @@ namespace MediaBrowser.Providers.Manager
             }
             }
         }
         }
 
 
+        /// <inheritdoc/>
         public Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>(RemoteSearchQuery<TLookupType> searchInfo, CancellationToken cancellationToken)
         public Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>(RemoteSearchQuery<TLookupType> searchInfo, CancellationToken cancellationToken)
             where TItemType : BaseItem, new()
             where TItemType : BaseItem, new()
             where TLookupType : ItemLookupInfo
             where TLookupType : ItemLookupInfo
@@ -748,7 +768,7 @@ namespace MediaBrowser.Providers.Manager
             return GetRemoteSearchResults<TItemType, TLookupType>(searchInfo, referenceItem, cancellationToken);
             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 TItemType : BaseItem, new()
             where TLookupType : ItemLookupInfo
             where TLookupType : ItemLookupInfo
         {
         {
@@ -837,7 +857,9 @@ namespace MediaBrowser.Providers.Manager
             return resultList;
             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)
             CancellationToken cancellationToken)
             where TLookupType : ItemLookupInfo
             where TLookupType : ItemLookupInfo
         {
         {
@@ -853,6 +875,7 @@ namespace MediaBrowser.Providers.Manager
             return list;
             return list;
         }
         }
 
 
+        /// <inheritdoc/>
         public Task<HttpResponseInfo> GetSearchImage(string providerName, string url, CancellationToken cancellationToken)
         public Task<HttpResponseInfo> GetSearchImage(string providerName, string url, CancellationToken cancellationToken)
         {
         {
             var provider = _metadataProviders.OfType<IRemoteSearchProvider>().FirstOrDefault(i => string.Equals(i.Name, providerName, StringComparison.OrdinalIgnoreCase));
             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);
             return provider.GetImageResponse(url, cancellationToken);
         }
         }
 
 
+        /// <inheritdoc/>
         public IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item)
         public IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item)
         {
         {
             return _externalIds.Where(i =>
             return _externalIds.Where(i =>
@@ -881,6 +905,7 @@ namespace MediaBrowser.Providers.Manager
             });
             });
         }
         }
 
 
+        /// <inheritdoc/>
         public IEnumerable<ExternalUrl> GetExternalUrls(BaseItem item)
         public IEnumerable<ExternalUrl> GetExternalUrls(BaseItem item)
         {
         {
             return GetExternalIds(item)
             return GetExternalIds(item)
@@ -909,6 +934,7 @@ namespace MediaBrowser.Providers.Manager
             }).Where(i => i != null).Concat(item.GetRelatedUrls());
             }).Where(i => i != null).Concat(item.GetRelatedUrls());
         }
         }
 
 
+        /// <inheritdoc/>
         public IEnumerable<ExternalIdInfo> GetExternalIdInfos(IHasProviderIds item)
         public IEnumerable<ExternalIdInfo> GetExternalIdInfos(IHasProviderIds item)
         {
         {
             return GetExternalIds(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()
         public Dictionary<Guid, Guid> GetRefreshQueue()
         {
         {
             lock (_refreshQueueLock)
             lock (_refreshQueueLock)
@@ -938,6 +963,7 @@ namespace MediaBrowser.Providers.Manager
             }
             }
         }
         }
 
 
+        /// <inheritdoc/>
         public void OnRefreshStart(BaseItem item)
         public void OnRefreshStart(BaseItem item)
         {
         {
             _logger.LogInformation("OnRefreshStart {0}", item.Id.ToString("N", CultureInfo.InvariantCulture));
             _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));
             RefreshStarted?.Invoke(this, new GenericEventArgs<BaseItem>(item));
         }
         }
 
 
+        /// <inheritdoc/>
         public void OnRefreshComplete(BaseItem item)
         public void OnRefreshComplete(BaseItem item)
         {
         {
             _logger.LogInformation("OnRefreshComplete {0}", item.Id.ToString("N", CultureInfo.InvariantCulture));
             _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));
             RefreshCompleted?.Invoke(this, new GenericEventArgs<BaseItem>(item));
         }
         }
 
 
+        /// <inheritdoc/>
         public double? GetRefreshProgress(Guid id)
         public double? GetRefreshProgress(Guid id)
         {
         {
             if (_activeRefreshes.TryGetValue(id, out double value))
             if (_activeRefreshes.TryGetValue(id, out double value))
@@ -964,6 +992,7 @@ namespace MediaBrowser.Providers.Manager
             return null;
             return null;
         }
         }
 
 
+        /// <inheritdoc/>
         public void OnRefreshProgress(BaseItem item, double progress)
         public void OnRefreshProgress(BaseItem item, double progress)
         {
         {
             var id = item.Id;
             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)));
             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)
         public void QueueRefresh(Guid id, MetadataRefreshOptions options, RefreshPriority priority)
         {
         {
             if (_disposed)
             if (_disposed)
@@ -1032,7 +1056,7 @@ namespace MediaBrowser.Providers.Manager
                     if (item != null)
                     if (item != null)
                     {
                     {
                         // Try to throttle this a little bit.
                         // 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
                         var task = item is MusicArtist artist
                             ? RefreshArtist(artist, refreshItem.Item2, cancellationToken)
                             ? RefreshArtist(artist, refreshItem.Item2, cancellationToken)
@@ -1062,17 +1086,14 @@ namespace MediaBrowser.Providers.Manager
             await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
             await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
 
 
             // Collection folders don't validate their children so we'll have to simulate that here
             // 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);
                     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.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)
         public Task RefreshFullItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
         {
             return RefreshItem(item, options, cancellationToken);
             return RefreshItem(item, options, cancellationToken);
         }
         }
 
 
-        private bool _disposed;
+        /// <inheritdoc/>
         public void Dispose()
         public void Dispose()
         {
         {
             _disposed = true;
             _disposed = true;

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

@@ -16,8 +16,8 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <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="OptimizedPriorityQueue" Version="4.2.0" />
     <PackageReference Include="PlaylistsNET" Version="1.0.6" />
     <PackageReference Include="PlaylistsNET" Version="1.0.6" />
     <PackageReference Include="TvDbSharper" Version="3.2.0" />
     <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"
                 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>
         </script>
     </div>
     </div>
 </body>
 </body>

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

@@ -36,33 +36,47 @@
                 uniquePluginId: "8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a"
                 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>
         </script>
     </div>
     </div>
 </body>
 </body>

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

@@ -24,25 +24,28 @@
                 pluginId: "a628c0da-fac5-4c7e-9d1a-7134223f14c8"
                 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>
         </script>
     </div>
     </div>
 </body>
 </body>

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

@@ -4,6 +4,7 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using System.Reflection;
 using System.Reflection;
+using System.Runtime.CompilerServices;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
@@ -229,6 +230,45 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
             return GetEpisodesPageAsync(tvdbId, 1, episodeQuery, language, cancellationToken);
             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)
         private async Task<T> TryGetValue<T>(string key, string language, Func<Task<T>> resultFactory)
         {
         {
             if (_cache.TryGetValue(key, out T cachedValue))
             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 language = item.GetPreferredMetadataLanguage();
             var remoteImages = new List<RemoteImageInfo>();
             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
                 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 language = item.GetPreferredMetadataLanguage();
             var remoteImages = new List<RemoteImageInfo>();
             var remoteImages = new List<RemoteImageInfo>();
-            var keyTypes = new[] { KeyType.Poster, KeyType.Series, KeyType.Fanart };
             var tvdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tvdb));
             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
                 var imageQuery = new ImagesQuery
                 {
                 {

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

@@ -247,10 +247,15 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
                 {
                 {
                     Name = tvdbTitles.FirstOrDefault(),
                     Name = tvdbTitles.FirstOrDefault(),
                     ProductionYear = firstAired.Year,
                     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
                 try
                 {
                 {
                     var seriesSesult =
                     var seriesSesult =
@@ -365,10 +370,14 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
                     Type = PersonType.Actor,
                     Type = PersonType.Actor,
                     Name = (actor.Name ?? string.Empty).Trim(),
                     Name = (actor.Name ?? string.Empty).Trim(),
                     Role = actor.Role,
                     Role = actor.Role,
-                    ImageUrl = TvdbUtils.BannerUrl + actor.Image,
                     SortOrder = actor.SortOrder
                     SortOrder = actor.SortOrder
                 };
                 };
 
 
+                if (!string.IsNullOrEmpty(actor.Image))
+                {
+                    personInfo.ImageUrl = TvdbUtils.BannerUrl + actor.Image;
+                }
+
                 if (!string.IsNullOrWhiteSpace(personInfo.Name))
                 if (!string.IsNullOrWhiteSpace(personInfo.Name))
                 {
                 {
                     result.AddPerson(personInfo);
                     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 TvdbApiKey = "OG4V3YJ3FAP7FP2K";
         public const string TvdbBaseUrl = "https://www.thetvdb.com/";
         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)
         public static ImageType GetImageTypeFromKeyType(string keyType)
         {
         {

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

@@ -58,7 +58,7 @@ namespace MediaBrowser.Providers.TV
             }
             }
             catch (Exception ex)
             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
 jellyfin-server (10.6.0-1) unstable; urgency=medium
 
 
   * Forthcoming stable release
   * 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"
 #JELLYFIN_SERVICE_OPT="--service"
 
 
 # [OPTIONAL] run Jellyfin without the web app
 # [OPTIONAL] run Jellyfin without the web app
-#JELLYFIN_NOWEBAPP_OPT="--noautorunwebapp"
+#JELLYFIN_NOWEBAPP_OPT="--nowebclient"
 
 
 #
 #
 # SysV init/Upstart options
 # SysV init/Upstart options
@@ -40,4 +40,4 @@ JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
 # Application username
 # Application username
 JELLYFIN_USER="jellyfin"
 JELLYFIN_USER="jellyfin"
 # Full application command
 # 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
 Vcs-Browser: https://github.org/jellyfin/jellyfin
 
 
 Package: jellyfin-server
 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
 Architecture: any
 Depends: at,
 Depends: at,
          libsqlite3-0,
          libsqlite3-0,

+ 4 - 6
deployment/build.windows.amd64

@@ -8,8 +8,7 @@ set -o xtrace
 # Version variables
 # Version variables
 NSSM_VERSION="nssm-2.24-101-g897c7ad"
 NSSM_VERSION="nssm-2.24-101-g897c7ad"
 NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip"
 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
 # Move to source directory
 pushd ${SOURCE_DIR}
 pushd ${SOURCE_DIR}
@@ -29,12 +28,11 @@ dotnet publish Jellyfin.Server --configuration Release --self-contained --runtim
 # Prepare addins
 # Prepare addins
 addin_build_dir="$( mktemp -d )"
 addin_build_dir="$( mktemp -d )"
 wget ${NSSM_URL} -O ${addin_build_dir}/nssm.zip
 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}
 unzip ${addin_build_dir}/nssm.zip -d ${addin_build_dir}
 cp ${addin_build_dir}/${NSSM_VERSION}/win64/nssm.exe ${output_dir}/nssm.exe
 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}
 rm -rf ${addin_build_dir}
 
 
 # Prepare scripts
 # 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" Version="4.13.0" />
     <PackageReference Include="AutoFixture.AutoMoq" Version="4.12.0" />
     <PackageReference Include="AutoFixture.AutoMoq" Version="4.12.0" />
     <PackageReference Include="AutoFixture.Xunit2" 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="Microsoft.NET.Test.Sdk" Version="16.6.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" 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>
   </PropertyGroup>
 
 
   <ItemGroup>
   <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="Microsoft.NET.Test.Sdk" Version="16.6.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" 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(
     param(
         [string]$ResolvedInstallLocation,
         [string]$ResolvedInstallLocation,
         [string]$Architecture,
         [string]$Architecture,
-        [string]$FFMPEGVersionX86 = "ffmpeg-4.2.1-win32-shared"
+        [string]$FFMPEGVersionX86 = "ffmpeg-4.3-win32-shared"
     )
     )
     Write-Verbose "Checking Architecture"
     Write-Verbose "Checking Architecture"
     if($Architecture -notin @('x86','x64')){
     if($Architecture -notin @('x86','x64')){