Browse Source

Merge branch 'master' into readonlyspan

Bond-009 5 years ago
parent
commit
3ff110984a
49 changed files with 788 additions and 626 deletions
  1. 5 2
      Emby.Dlna/Api/DlnaServerService.cs
  2. 9 5
      Emby.Dlna/Eventing/EventManager.cs
  3. 2 2
      Emby.Dlna/PlayTo/Device.cs
  4. 75 107
      Emby.Dlna/Server/DescriptionXmlBuilder.cs
  5. 25 9
      Emby.Dlna/Service/ServiceXmlBuilder.cs
  6. 30 30
      Emby.Naming/Common/NamingOptions.cs
  7. 2 3
      Emby.Server.Implementations/ApplicationHost.cs
  8. 9 5
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  9. 0 2
      Emby.Server.Implementations/HttpServer/FileWriter.cs
  10. 0 4
      Emby.Server.Implementations/HttpServer/Security/AuthService.cs
  11. 2 8
      Emby.Server.Implementations/Images/ArtistImageProvider.cs
  12. 1 1
      Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
  13. 12 12
      Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
  14. 103 112
      Emby.Server.Implementations/Library/LibraryManager.cs
  15. 12 12
      Emby.Server.Implementations/Library/LiveStreamHelper.cs
  16. 8 8
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  17. 1 1
      Emby.Server.Implementations/Library/MediaStreamSelector.cs
  18. 0 5
      Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
  19. 6 12
      Emby.Server.Implementations/Library/SearchEngine.cs
  20. 0 4
      Emby.Server.Implementations/Library/UserDataManager.cs
  21. 3 6
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  22. 1 6
      Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
  23. 0 8
      Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
  24. 0 1
      Emby.Server.Implementations/Services/ServiceController.cs
  25. 3 1
      Emby.Server.Implementations/Services/ServiceHandler.cs
  26. 4 2
      Emby.Server.Implementations/Services/ServicePath.cs
  27. 3 5
      Jellyfin.Api/Controllers/StartupController.cs
  28. 13 0
      Jellyfin.Server.Implementations/JellyfinDb.cs
  29. 9 7
      Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
  30. 57 40
      Jellyfin.Server.Implementations/Users/UserManager.cs
  31. 3 2
      Jellyfin.Server/CoreAppHost.cs
  32. 2 1
      Jellyfin.Server/Migrations/MigrationRunner.cs
  33. 49 0
      Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs
  34. 15 0
      Jellyfin.Server/Program.cs
  35. 1 1
      MediaBrowser.Api/UserService.cs
  36. 33 0
      MediaBrowser.Common/Extensions/HttpContextExtensions.cs
  37. 1 1
      MediaBrowser.Controller/Library/ILibraryManager.cs
  38. 2 5
      MediaBrowser.Controller/Library/IUserManager.cs
  39. 58 34
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  40. 2 2
      MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
  41. 4 2
      MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
  42. 129 90
      MediaBrowser.Model/Entities/MediaStream.cs
  43. 10 3
      MediaBrowser.Model/Notifications/NotificationOption.cs
  44. 2 2
      MediaBrowser.Providers/Manager/ProviderManager.cs
  45. 22 20
      MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html
  46. 38 24
      MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html
  47. 19 16
      MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html
  48. 1 1
      MediaBrowser.Providers/TV/SeriesMetadataService.cs
  49. 2 2
      debian/conf/jellyfin

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 0 - 2
Emby.Server.Implementations/HttpServer/FileWriter.cs

@@ -29,7 +29,6 @@ namespace Emby.Server.Implementations.HttpServer
 
         private readonly IStreamHelper _streamHelper;
         private readonly ILogger _logger;
-        private readonly IFileSystem _fileSystem;
 
         /// <summary>
         /// The _options.
@@ -49,7 +48,6 @@ namespace Emby.Server.Implementations.HttpServer
             }
 
             _streamHelper = streamHelper;
-            _fileSystem = fileSystem;
 
             Path = path;
             _logger = logger;

+ 0 - 4
Emby.Server.Implementations/HttpServer/Security/AuthService.cs

@@ -13,26 +13,22 @@ using MediaBrowser.Controller.Security;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Services;
 using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.HttpServer.Security
 {
     public class AuthService : IAuthService
     {
-        private readonly ILogger<AuthService> _logger;
         private readonly IAuthorizationContext _authorizationContext;
         private readonly ISessionManager _sessionManager;
         private readonly IServerConfigurationManager _config;
         private readonly INetworkManager _networkManager;
 
         public AuthService(
-            ILogger<AuthService> logger,
             IAuthorizationContext authorizationContext,
             IServerConfigurationManager config,
             ISessionManager sessionManager,
             INetworkManager networkManager)
         {
-            _logger = logger;
             _authorizationContext = authorizationContext;
             _config = config;
             _sessionManager = sessionManager;

+ 2 - 8
Emby.Server.Implementations/Images/ArtistImageProvider.cs

@@ -11,7 +11,6 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
@@ -25,14 +24,9 @@ namespace Emby.Server.Implementations.Images
     /// </summary>
     public class ArtistImageProvider : BaseDynamicImageProvider<MusicArtist>
     {
-        /// <summary>
-        /// The library manager.
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
-
-        public ArtistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
+        public ArtistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor)
+            : base(fileSystem, providerManager, applicationPaths, imageProcessor)
         {
-            _libraryManager = libraryManager;
         }
 
         /// <summary>

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

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

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

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

+ 103 - 112
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -60,6 +60,8 @@ namespace Emby.Server.Implementations.Library
     /// </summary>
     public class LibraryManager : ILibraryManager
     {
+        private const string ShortcutFileExtension = ".mblink";
+
         private readonly ILogger<LibraryManager> _logger;
         private readonly ITaskManager _taskManager;
         private readonly IUserManager _userManager;
@@ -75,63 +77,24 @@ namespace Emby.Server.Implementations.Library
         private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
         private readonly IImageProcessor _imageProcessor;
 
-        private NamingOptions _namingOptions;
-        private string[] _videoFileExtensions;
-
-        private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
-
-        private IProviderManager ProviderManager => _providerManagerFactory.Value;
-
-        private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
-
-        /// <summary>
-        /// Gets or sets the postscan tasks.
-        /// </summary>
-        /// <value>The postscan tasks.</value>
-        private ILibraryPostScanTask[] PostscanTasks { get; set; }
-
-        /// <summary>
-        /// Gets or sets the intro providers.
-        /// </summary>
-        /// <value>The intro providers.</value>
-        private IIntroProvider[] IntroProviders { get; set; }
-
-        /// <summary>
-        /// Gets or sets the list of entity resolution ignore rules.
-        /// </summary>
-        /// <value>The entity resolution ignore rules.</value>
-        private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
-
-        /// <summary>
-        /// Gets or sets the list of currently registered entity resolvers.
-        /// </summary>
-        /// <value>The entity resolvers enumerable.</value>
-        private IItemResolver[] EntityResolvers { get; set; }
-
-        private IMultiItemResolver[] MultiItemResolvers { get; set; }
-
         /// <summary>
-        /// Gets or sets the comparers.
+        /// The _root folder sync lock.
         /// </summary>
-        /// <value>The comparers.</value>
-        private IBaseItemComparer[] Comparers { get; set; }
+        private readonly object _rootFolderSyncLock = new object();
+        private readonly object _userRootFolderSyncLock = new object();
 
-        /// <summary>
-        /// Occurs when [item added].
-        /// </summary>
-        public event EventHandler<ItemChangeEventArgs> ItemAdded;
+        private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
 
-        /// <summary>
-        /// Occurs when [item updated].
-        /// </summary>
-        public event EventHandler<ItemChangeEventArgs> ItemUpdated;
+        private NamingOptions _namingOptions;
+        private string[] _videoFileExtensions;
 
         /// <summary>
-        /// Occurs when [item removed].
+        /// The _root folder.
         /// </summary>
-        public event EventHandler<ItemChangeEventArgs> ItemRemoved;
+        private volatile AggregateFolder _rootFolder;
+        private volatile UserRootFolder _userRootFolder;
 
-        public bool IsScanRunning { get; private set; }
+        private bool _wizardCompleted;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="LibraryManager" /> class.
@@ -186,37 +149,19 @@ namespace Emby.Server.Implementations.Library
         }
 
         /// <summary>
-        /// Adds the parts.
+        /// Occurs when [item added].
         /// </summary>
-        /// <param name="rules">The rules.</param>
-        /// <param name="resolvers">The resolvers.</param>
-        /// <param name="introProviders">The intro providers.</param>
-        /// <param name="itemComparers">The item comparers.</param>
-        /// <param name="postscanTasks">The post scan tasks.</param>
-        public void AddParts(
-            IEnumerable<IResolverIgnoreRule> rules,
-            IEnumerable<IItemResolver> resolvers,
-            IEnumerable<IIntroProvider> introProviders,
-            IEnumerable<IBaseItemComparer> itemComparers,
-            IEnumerable<ILibraryPostScanTask> postscanTasks)
-        {
-            EntityResolutionIgnoreRules = rules.ToArray();
-            EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
-            MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
-            IntroProviders = introProviders.ToArray();
-            Comparers = itemComparers.ToArray();
-            PostscanTasks = postscanTasks.ToArray();
-        }
+        public event EventHandler<ItemChangeEventArgs> ItemAdded;
 
         /// <summary>
-        /// The _root folder.
+        /// Occurs when [item updated].
         /// </summary>
-        private volatile AggregateFolder _rootFolder;
+        public event EventHandler<ItemChangeEventArgs> ItemUpdated;
 
         /// <summary>
-        /// The _root folder sync lock.
+        /// Occurs when [item removed].
         /// </summary>
-        private readonly object _rootFolderSyncLock = new object();
+        public event EventHandler<ItemChangeEventArgs> ItemRemoved;
 
         /// <summary>
         /// Gets the root folder.
@@ -241,7 +186,68 @@ namespace Emby.Server.Implementations.Library
             }
         }
 
-        private bool _wizardCompleted;
+        private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
+
+        private IProviderManager ProviderManager => _providerManagerFactory.Value;
+
+        private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
+
+        /// <summary>
+        /// Gets or sets the postscan tasks.
+        /// </summary>
+        /// <value>The postscan tasks.</value>
+        private ILibraryPostScanTask[] PostscanTasks { get; set; }
+
+        /// <summary>
+        /// Gets or sets the intro providers.
+        /// </summary>
+        /// <value>The intro providers.</value>
+        private IIntroProvider[] IntroProviders { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of entity resolution ignore rules.
+        /// </summary>
+        /// <value>The entity resolution ignore rules.</value>
+        private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of currently registered entity resolvers.
+        /// </summary>
+        /// <value>The entity resolvers enumerable.</value>
+        private IItemResolver[] EntityResolvers { get; set; }
+
+        private IMultiItemResolver[] MultiItemResolvers { get; set; }
+
+        /// <summary>
+        /// Gets or sets the comparers.
+        /// </summary>
+        /// <value>The comparers.</value>
+        private IBaseItemComparer[] Comparers { get; set; }
+
+        public bool IsScanRunning { get; private set; }
+
+        /// <summary>
+        /// Adds the parts.
+        /// </summary>
+        /// <param name="rules">The rules.</param>
+        /// <param name="resolvers">The resolvers.</param>
+        /// <param name="introProviders">The intro providers.</param>
+        /// <param name="itemComparers">The item comparers.</param>
+        /// <param name="postscanTasks">The post scan tasks.</param>
+        public void AddParts(
+            IEnumerable<IResolverIgnoreRule> rules,
+            IEnumerable<IItemResolver> resolvers,
+            IEnumerable<IIntroProvider> introProviders,
+            IEnumerable<IBaseItemComparer> itemComparers,
+            IEnumerable<ILibraryPostScanTask> postscanTasks)
+        {
+            EntityResolutionIgnoreRules = rules.ToArray();
+            EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
+            MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
+            IntroProviders = introProviders.ToArray();
+            Comparers = itemComparers.ToArray();
+            PostscanTasks = postscanTasks.ToArray();
+        }
 
         /// <summary>
         /// Records the configuration values.
@@ -775,14 +781,11 @@ namespace Emby.Server.Implementations.Library
             return rootFolder;
         }
 
-        private volatile UserRootFolder _userRootFolder;
-        private readonly object _syncLock = new object();
-
         public Folder GetUserRootFolder()
         {
             if (_userRootFolder == null)
             {
-                lock (_syncLock)
+                lock (_userRootFolderSyncLock)
                 {
                     if (_userRootFolder == null)
                     {
@@ -1332,7 +1335,7 @@ namespace Emby.Server.Implementations.Library
 
             return new QueryResult<BaseItem>
             {
-                Items = _itemRepository.GetItemList(query).ToArray()
+                Items = _itemRepository.GetItemList(query)
             };
         }
 
@@ -1463,11 +1466,9 @@ namespace Emby.Server.Implementations.Library
                 return _itemRepository.GetItems(query);
             }
 
-            var list = _itemRepository.GetItemList(query);
-
             return new QueryResult<BaseItem>
             {
-                Items = list
+                Items = _itemRepository.GetItemList(query)
             };
         }
 
@@ -1876,7 +1877,8 @@ namespace Emby.Server.Implementations.Library
             }
 
             var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
-            if (outdated.Length == 0)
+            // Skip image processing if current or live tv source
+            if (outdated.Length == 0 || item.SourceType != SourceType.Library)
             {
                 RegisterItem(item);
                 return;
@@ -1945,12 +1947,9 @@ namespace Emby.Server.Implementations.Library
         /// <summary>
         /// Updates the item.
         /// </summary>
-        public void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
+        public void UpdateItems(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
         {
-            // Don't iterate multiple times
-            var itemsList = items.ToList();
-
-            foreach (var item in itemsList)
+            foreach (var item in items)
             {
                 if (item.IsFileProtocol)
                 {
@@ -1962,11 +1961,11 @@ namespace Emby.Server.Implementations.Library
                 UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate);
             }
 
-            _itemRepository.SaveItems(itemsList, cancellationToken);
+            _itemRepository.SaveItems(items, cancellationToken);
 
             if (ItemUpdated != null)
             {
-                foreach (var item in itemsList)
+                foreach (var item in items)
                 {
                     // With the live tv guide this just creates too much noise
                     if (item.SourceType != SourceType.Library)
@@ -2189,8 +2188,6 @@ namespace Emby.Server.Implementations.Library
                 .FirstOrDefault(i => !string.IsNullOrEmpty(i));
         }
 
-        private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
-
         public UserView GetNamedView(
             User user,
             string name,
@@ -2488,14 +2485,9 @@ namespace Emby.Server.Implementations.Library
 
             var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
 
-            var episodeInfo = episode.IsFileProtocol ?
-                resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) :
-                new Naming.TV.EpisodeInfo();
-
-            if (episodeInfo == null)
-            {
-                episodeInfo = new Naming.TV.EpisodeInfo();
-            }
+            var episodeInfo = episode.IsFileProtocol
+                ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo()
+                : new Naming.TV.EpisodeInfo();
 
             try
             {
@@ -2503,11 +2495,13 @@ namespace Emby.Server.Implementations.Library
                 if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase))
                 {
                     // Read from metadata
-                    var mediaInfo = _mediaEncoder.GetMediaInfo(new MediaInfoRequest
-                    {
-                        MediaSource = episode.GetMediaSources(false)[0],
-                        MediaType = DlnaProfileType.Video
-                    }, CancellationToken.None).GetAwaiter().GetResult();
+                    var mediaInfo = _mediaEncoder.GetMediaInfo(
+                        new MediaInfoRequest
+                        {
+                            MediaSource = episode.GetMediaSources(false)[0],
+                            MediaType = DlnaProfileType.Video
+                        },
+                        CancellationToken.None).GetAwaiter().GetResult();
                     if (mediaInfo.ParentIndexNumber > 0)
                     {
                         episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
@@ -2665,7 +2659,7 @@ namespace Emby.Server.Implementations.Library
 
             var videos = videoListResolver.Resolve(fileSystemChildren);
 
-            var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase));
+            var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
 
             if (currentVideo != null)
             {
@@ -2682,9 +2676,7 @@ namespace Emby.Server.Implementations.Library
                 .Select(video =>
                 {
                     // Try to retrieve it from the db. If we don't find it, use the resolved version
-                    var dbItem = GetItemById(video.Id) as Trailer;
-
-                    if (dbItem != null)
+                    if (GetItemById(video.Id) is Trailer dbItem)
                     {
                         video = dbItem;
                     }
@@ -3011,8 +3003,6 @@ namespace Emby.Server.Implementations.Library
             });
         }
 
-        private const string ShortcutFileExtension = ".mblink";
-
         public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
         {
             AddMediaPathInternal(virtualFolderName, pathInfo, true);
@@ -3206,7 +3196,8 @@ namespace Emby.Server.Implementations.Library
 
             if (!Directory.Exists(virtualFolderPath))
             {
-                throw new FileNotFoundException(string.Format("The media collection {0} does not exist", virtualFolderName));
+                throw new FileNotFoundException(
+                    string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolderName));
             }
 
             var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)

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

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

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

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

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

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

+ 0 - 5
Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs

@@ -1,6 +1,5 @@
 using System.Globalization;
 using Emby.Naming.TV;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Globalization;
@@ -13,7 +12,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
     /// </summary>
     public class SeasonResolver : FolderResolver<Season>
     {
-        private readonly IServerConfigurationManager _config;
         private readonly ILibraryManager _libraryManager;
         private readonly ILocalizationManager _localization;
         private readonly ILogger<SeasonResolver> _logger;
@@ -21,17 +19,14 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
         /// <summary>
         /// Initializes a new instance of the <see cref="SeasonResolver"/> class.
         /// </summary>
-        /// <param name="config">The config.</param>
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="localization">The localization.</param>
         /// <param name="logger">The logger.</param>
         public SeasonResolver(
-            IServerConfigurationManager config,
             ILibraryManager libraryManager,
             ILocalizationManager localization,
             ILogger<SeasonResolver> logger)
         {
-            _config = config;
             _libraryManager = libraryManager;
             _localization = localization;
             _logger = logger;

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

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

+ 0 - 4
Emby.Server.Implementations/Library/UserDataManager.cs

@@ -13,7 +13,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
-using Microsoft.Extensions.Logging;
 using Book = MediaBrowser.Controller.Entities.Book;
 
 namespace Emby.Server.Implementations.Library
@@ -28,18 +27,15 @@ namespace Emby.Server.Implementations.Library
         private readonly ConcurrentDictionary<string, UserItemData> _userData =
             new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
 
-        private readonly ILogger<UserDataManager> _logger;
         private readonly IServerConfigurationManager _config;
         private readonly IUserManager _userManager;
         private readonly IUserDataRepository _repository;
 
         public UserDataManager(
-            ILogger<UserDataManager> logger,
             IServerConfigurationManager config,
             IUserManager userManager,
             IUserDataRepository repository)
         {
-            _logger = logger;
             _config = config;
             _userManager = userManager;
             _repository = repository;

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

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

+ 1 - 6
Emby.Server.Implementations/ScheduledTasks/TaskManager.cs

@@ -7,7 +7,6 @@ using System.Linq;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Model.Events;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.Logging;
@@ -37,7 +36,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IApplicationPaths _applicationPaths;
         private readonly ILogger<TaskManager> _logger;
-        private readonly IFileSystem _fileSystem;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="TaskManager" /> class.
@@ -45,17 +43,14 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// <param name="applicationPaths">The application paths.</param>
         /// <param name="jsonSerializer">The json serializer.</param>
         /// <param name="logger">The logger.</param>
-        /// <param name="fileSystem">The filesystem manager.</param>
         public TaskManager(
             IApplicationPaths applicationPaths,
             IJsonSerializer jsonSerializer,
-            ILogger<TaskManager> logger,
-            IFileSystem fileSystem)
+            ILogger<TaskManager> logger)
         {
             _applicationPaths = applicationPaths;
             _jsonSerializer = jsonSerializer;
             _logger = logger;
-            _fileSystem = fileSystem;
 
             ScheduledTasks = Array.Empty<IScheduledTaskWorker>();
         }

+ 0 - 8
Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs

@@ -14,7 +14,6 @@ using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Tasks;
-using Microsoft.Extensions.Logging;
 using MediaBrowser.Model.Globalization;
 
 namespace Emby.Server.Implementations.ScheduledTasks
@@ -24,11 +23,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
     /// </summary>
     public class ChapterImagesTask : IScheduledTask
     {
-        /// <summary>
-        /// The _logger.
-        /// </summary>
-        private readonly ILogger<ChapterImagesTask> _logger;
-
         /// <summary>
         /// The _library manager.
         /// </summary>
@@ -46,7 +40,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
         /// </summary>
         public ChapterImagesTask(
-            ILoggerFactory loggerFactory,
             ILibraryManager libraryManager,
             IItemRepository itemRepo,
             IApplicationPaths appPaths,
@@ -54,7 +47,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
             IFileSystem fileSystem,
             ILocalizationManager localization)
         {
-            _logger = loggerFactory.CreateLogger<ChapterImagesTask>();
             _libraryManager = libraryManager;
             _itemRepo = itemRepo;
             _appPaths = appPaths;

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

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

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

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

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

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

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

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

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

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

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

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

+ 57 - 40
Jellyfin.Server.Implementations/Users/UserManager.cs

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

+ 3 - 2
Jellyfin.Server/CoreAppHost.cs

@@ -9,6 +9,7 @@ using Jellyfin.Server.Implementations;
 using Jellyfin.Server.Implementations.Activity;
 using Jellyfin.Server.Implementations.Users;
 using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Activity;
@@ -33,9 +34,9 @@ namespace Jellyfin.Server
         /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param>
         public CoreAppHost(
-            ServerApplicationPaths applicationPaths,
+            IServerApplicationPaths applicationPaths,
             ILoggerFactory loggerFactory,
-            StartupOptions options,
+            IStartupOptions options,
             IFileSystem fileSystem,
             INetworkManager networkManager)
             : base(

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

@@ -21,7 +21,8 @@ namespace Jellyfin.Server.Migrations
             typeof(Routines.MigrateActivityLogDb),
             typeof(Routines.RemoveDuplicateExtras),
             typeof(Routines.AddDefaultPluginRepository),
-            typeof(Routines.MigrateUserDb)
+            typeof(Routines.MigrateUserDb),
+            typeof(Routines.ReaddDefaultPluginRepository)
         };
 
         /// <summary>

+ 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();
+            }
+        }
+    }
+}

+ 15 - 0
Jellyfin.Server/Program.cs

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

+ 1 - 1
MediaBrowser.Api/UserService.cs

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

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

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

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

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

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

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

+ 58 - 34
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -372,7 +372,7 @@ namespace MediaBrowser.Controller.MediaEncoding
         public int GetVideoProfileScore(string profile)
         {
             // strip spaces because they may be stripped out on the query string
-            profile = profile.Replace(" ", "");
+            profile = profile.Replace(" ", string.Empty, StringComparison.Ordinal);
             return Array.FindIndex(_videoProfiles, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase));
         }
 
@@ -460,7 +460,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             if (!IsCopyCodec(outputVideoCodec))
             {
                 if (state.IsVideoRequest
-                    && IsVaapiSupported(state)
+                    && _mediaEncoder.SupportsHwaccel("vaapi")
                     && string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
                 {
                     if (isVaapiDecoder)
@@ -468,13 +468,13 @@ namespace MediaBrowser.Controller.MediaEncoding
                         arg.Append("-hwaccel_output_format vaapi ")
                             .Append("-vaapi_device ")
                             .Append(encodingOptions.VaapiDevice)
-                            .Append(" ");
+                            .Append(' ');
                     }
                     else if (!isVaapiDecoder && isVaapiEncoder)
                     {
                         arg.Append("-vaapi_device ")
                             .Append(encodingOptions.VaapiDevice)
-                            .Append(" ");
+                            .Append(' ');
                     }
                 }
 
@@ -495,13 +495,13 @@ namespace MediaBrowser.Controller.MediaEncoding
                                 }
                                 else
                                 {
-                                    arg.Append("-hwaccel qsv -init_hw_device qsv=hw ");
+                                    arg.Append("-hwaccel qsv ");
                                 }
                             }
 
                             if (isWindows)
                             {
-                                arg.Append("-hwaccel qsv -init_hw_device qsv=hw ");
+                                arg.Append("-hwaccel qsv ");
                             }
                         }
                         // While using SW decoder
@@ -1606,25 +1606,41 @@ namespace MediaBrowser.Controller.MediaEncoding
                 }
                 else
                 {
-                    index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase);
+                    index = outputSizeParam.IndexOf("hwupload=extra_hw_frames", StringComparison.OrdinalIgnoreCase);
                     if (index != -1)
                     {
                         outputSizeParam = outputSizeParam.Slice(index);
                     }
                     else
                     {
-                        index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase);
+                        index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase);
                         if (index != -1)
                         {
                             outputSizeParam = outputSizeParam.Slice(index);
                         }
                         else
                         {
-                            index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase);
+                            index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase);
                             if (index != -1)
                             {
                                 outputSizeParam = outputSizeParam.Slice(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);
+                                    }
+                                }
+                            }
                         }
                     }
                 }
@@ -1685,7 +1701,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             }
 
             // 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))
             {
                 /*
@@ -1790,6 +1806,10 @@ namespace MediaBrowser.Controller.MediaEncoding
                 var outputWidth = width.Value;
                 var outputHeight = height.Value;
                 var qsv_or_vaapi = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase);
+                var isDeintEnabled = state.DeInterlace("h264", true)
+                    || state.DeInterlace("avc", true)
+                    || state.DeInterlace("h265", true)
+                    || state.DeInterlace("hevc", true);
 
                 if (!videoWidth.HasValue
                     || outputWidth != videoWidth.Value
@@ -1805,7 +1825,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                             qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi",
                             outputWidth,
                             outputHeight,
-                            (qsv_or_vaapi && state.DeInterlace("h264", true)) ? ":deinterlace=1" : string.Empty));
+                            (qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty));
                 }
                 else
                 {
@@ -1814,7 +1834,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                             CultureInfo.InvariantCulture,
                             "{0}=format=nv12{1}",
                             qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi",
-                            (qsv_or_vaapi && state.DeInterlace("h264", true)) ? ":deinterlace=1" : string.Empty));
+                            (qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty));
                 }
             }
             else if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1
@@ -2026,7 +2046,6 @@ namespace MediaBrowser.Controller.MediaEncoding
             // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/
 
             var request = state.BaseRequest;
-
             var videoStream = state.VideoStream;
             var filters = new List<string>();
 
@@ -2035,23 +2054,31 @@ namespace MediaBrowser.Controller.MediaEncoding
             var inputHeight = videoStream?.Height;
             var threeDFormat = state.MediaSource.Video3DFormat;
 
+            var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
+            var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
+            var isQsvH264Encoder = outputVideoCodec.IndexOf("h264_qsv", StringComparison.OrdinalIgnoreCase) != -1;
+            var isNvdecH264Decoder = videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
+            var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1;
+            var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+
+            var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+            var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+
             // When the input may or may not be hardware VAAPI decodable
-            if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+            if (isVaapiH264Encoder)
             {
                 filters.Add("format=nv12|vaapi");
                 filters.Add("hwupload");
             }
 
-            // When the input may or may not be hardware QSV decodable
-            else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+            // When burning in graphical subtitles using overlay_qsv, upload videostream to the same qsv context
+            else if (isLinux && hasGraphicalSubs && isQsvH264Encoder)
             {
-                filters.Add("format=nv12|qsv");
                 filters.Add("hwupload=extra_hw_frames=64");
             }
 
             // 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
-                && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
+            else if (IsVaapiSupported(state) && isVaapiDecoder && isLibX264Encoder)
             {
                 var codec = videoStream.Codec.ToLowerInvariant();
                 var isColorDepth10 = IsColorDepth10(state);
@@ -2079,16 +2106,19 @@ namespace MediaBrowser.Controller.MediaEncoding
             }
 
             // Add hardware deinterlace filter before scaling filter
-            if (state.DeInterlace("h264", true))
+            if (state.DeInterlace("h264", true) || state.DeInterlace("avc", true))
             {
-                if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+                if (isVaapiH264Encoder)
                 {
                     filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_vaapi"));
                 }
             }
 
             // Add software deinterlace filter before scaling filter
-            if (state.DeInterlace("h264", true) || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true))
+            if (state.DeInterlace("h264", true)
+                || state.DeInterlace("avc", true)
+                || state.DeInterlace("h265", true)
+                || state.DeInterlace("hevc", true))
             {
                 string deintParam;
                 var inputFramerate = videoStream?.RealFrameRate;
@@ -2105,9 +2135,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 
                 if (!string.IsNullOrEmpty(deintParam))
                 {
-                    if (!string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
-                        && !string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
-                        && videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) == -1)
+                    if (!isVaapiH264Encoder && !isQsvH264Encoder && !isNvdecH264Decoder)
                     {
                         filters.Add(deintParam);
                     }
@@ -2117,12 +2145,10 @@ namespace MediaBrowser.Controller.MediaEncoding
             // Add scaling filter: scale_*=format=nv12 or scale_*=w=*:h=*:format=nv12 or scale=expr
             filters.AddRange(GetScalingFilters(state, inputWidth, inputHeight, threeDFormat, videoDecoder, outputVideoCodec, request.Width, request.Height, request.MaxWidth, request.MaxHeight));
 
-            // Add parameters to use VAAPI with burn-in text subttiles (GH issue #642)
-            if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+            // Add parameters to use VAAPI with burn-in text subtitles (GH issue #642)
+            if (isVaapiH264Encoder)
             {
-                if (state.SubtitleStream != null
-                    && state.SubtitleStream.IsTextSubtitleStream
-                    && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+                if (hasTextSubs)
                 {
                     // Test passed on Intel and AMD gfx
                     filters.Add("hwmap=mode=read+write");
@@ -2132,9 +2158,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 
             var output = string.Empty;
 
-            if (state.SubtitleStream != null
-                && state.SubtitleStream.IsTextSubtitleStream
-                && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+            if (hasTextSubs)
             {
                 var subParam = GetTextSubtitleParam(state);
 
@@ -2142,7 +2166,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 
                 // Ensure proper filters are passed to ffmpeg in case of hardware acceleration via VA-API
                 // Reference: https://trac.ffmpeg.org/wiki/Hardware/VAAPI
-                if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+                if (isVaapiH264Encoder)
                 {
                     filters.Add("hwmap");
                 }

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

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

+ 4 - 2
MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs

@@ -174,7 +174,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                 inputFiles = new[] { mediaSource.Path };
             }
 
-            var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, mediaSource.Protocol, subtitleStream, cancellationToken).ConfigureAwait(false);
+            var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), subtitleStream, cancellationToken).ConfigureAwait(false);
 
             var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false);
 
@@ -428,7 +428,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
 
             // FFmpeg automatically convert character encoding when it is UTF-16
             // If we specify character encoding, it rejects with "do not specify a character encoding" and "Unable to recode subtitle event"
-            if ((inputPath.EndsWith(".smi") || inputPath.EndsWith(".sami")) && (encodingParam == "UTF-16BE" || encodingParam == "UTF-16LE"))
+            if ((inputPath.EndsWith(".smi") || inputPath.EndsWith(".sami")) &&
+                (encodingParam.Equals("UTF-16BE", StringComparison.OrdinalIgnoreCase) ||
+                 encodingParam.Equals("UTF-16LE", StringComparison.OrdinalIgnoreCase)))
             {
                 encodingParam = "";
             }

+ 129 - 90
MediaBrowser.Model/Entities/MediaStream.cs

@@ -112,107 +112,146 @@ namespace MediaBrowser.Model.Entities
         {
             get
             {
-                if (Type == MediaStreamType.Audio)
+                switch (Type)
                 {
-                    // if (!string.IsNullOrEmpty(Title))
-                    //{
-                    //    return AddLanguageIfNeeded(Title);
-                    //}
-
-                    var attributes = new List<string>();
-
-                    if (!string.IsNullOrEmpty(Language))
-                    {
-                        attributes.Add(StringHelper.FirstToUpper(Language));
-                    }
-
-                    if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase))
-                    {
-                        attributes.Add(AudioCodec.GetFriendlyName(Codec));
-                    }
-                    else if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase))
-                    {
-                        attributes.Add(Profile);
-                    }
-
-                    if (!string.IsNullOrEmpty(ChannelLayout))
-                    {
-                        attributes.Add(ChannelLayout);
-                    }
-                    else if (Channels.HasValue)
+                    case MediaStreamType.Audio:
                     {
-                        attributes.Add(Channels.Value.ToString(CultureInfo.InvariantCulture) + " ch");
+                        var attributes = new List<string>();
+
+                        if (!string.IsNullOrEmpty(Language))
+                        {
+                            // Get full language string i.e. eng -> English. Will not work for some languages which use ISO 639-2/B instead of /T codes.
+                            string fullLanguage = CultureInfo
+                                .GetCultures(CultureTypes.NeutralCultures)
+                                .FirstOrDefault(r => r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase))
+                                ?.DisplayName;
+                            attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
+                        }
+
+                        if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase))
+                        {
+                            attributes.Add(AudioCodec.GetFriendlyName(Codec));
+                        }
+                        else if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase))
+                        {
+                            attributes.Add(Profile);
+                        }
+
+                        if (!string.IsNullOrEmpty(ChannelLayout))
+                        {
+                            attributes.Add(StringHelper.FirstToUpper(ChannelLayout));
+                        }
+                        else if (Channels.HasValue)
+                        {
+                            attributes.Add(Channels.Value.ToString(CultureInfo.InvariantCulture) + " ch");
+                        }
+
+                        if (IsDefault)
+                        {
+                            attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault);
+                        }
+
+                        if (!string.IsNullOrEmpty(Title))
+                        {
+                            var result = new StringBuilder(Title);
+                            foreach (var tag in attributes)
+                            {
+                                // Keep Tags that are not already in Title.
+                                if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1)
+                                {
+                                    result.Append(" - ").Append(tag);
+                                }
+                            }
+
+                            return result.ToString();
+                        }
+
+                        return string.Join(" - ", attributes);
                     }
 
-                    if (IsDefault)
+                    case MediaStreamType.Video:
                     {
-                        attributes.Add("Default");
+                        var attributes = new List<string>();
+
+                        var resolutionText = GetResolutionText();
+
+                        if (!string.IsNullOrEmpty(resolutionText))
+                        {
+                            attributes.Add(resolutionText);
+                        }
+
+                        if (!string.IsNullOrEmpty(Codec))
+                        {
+                            attributes.Add(Codec.ToUpperInvariant());
+                        }
+
+                        if (!string.IsNullOrEmpty(Title))
+                        {
+                            var result = new StringBuilder(Title);
+                            foreach (var tag in attributes)
+                            {
+                                // Keep Tags that are not already in Title.
+                                if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1)
+                                {
+                                    result.Append(" - ").Append(tag);
+                                }
+                            }
+
+                            return result.ToString();
+                        }
+
+                        return string.Join(" ", attributes);
                     }
 
-                    return string.Join(" ", attributes);
-                }
-
-                if (Type == MediaStreamType.Video)
-                {
-                    var attributes = new List<string>();
-
-                    var resolutionText = GetResolutionText();
-
-                    if (!string.IsNullOrEmpty(resolutionText))
+                    case MediaStreamType.Subtitle:
                     {
-                        attributes.Add(resolutionText);
+                        var attributes = new List<string>();
+
+                        if (!string.IsNullOrEmpty(Language))
+                        {
+                            // Get full language string i.e. eng -> English. Will not work for some languages which use ISO 639-2/B instead of /T codes.
+                            string fullLanguage = CultureInfo
+                                .GetCultures(CultureTypes.NeutralCultures)
+                                .FirstOrDefault(r => r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase))
+                                ?.DisplayName;
+                            attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
+                        }
+                        else
+                        {
+                            attributes.Add(string.IsNullOrEmpty(localizedUndefined) ? "Und" : localizedUndefined);
+                        }
+
+                        if (IsDefault)
+                        {
+                            attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault);
+                        }
+
+                        if (IsForced)
+                        {
+                            attributes.Add(string.IsNullOrEmpty(localizedForced) ? "Forced" : localizedForced);
+                        }
+
+                        if (!string.IsNullOrEmpty(Title))
+                        {
+                           var result = new StringBuilder(Title);
+                            foreach (var tag in attributes)
+                            {
+                                // Keep Tags that are not already in Title.
+                                if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1)
+                                {
+                                    result.Append(" - ").Append(tag);
+                                }
+                            }
+
+                            return result.ToString();
+                        }
+
+                        return string.Join(" - ", attributes.ToArray());
                     }
 
-                    if (!string.IsNullOrEmpty(Codec))
-                    {
-                        attributes.Add(Codec.ToUpperInvariant());
-                    }
-
-                    return string.Join(" ", attributes);
+                    default:
+                        return null;
                 }
-
-                if (Type == MediaStreamType.Subtitle)
-                {
-
-                    var attributes = new List<string>();
-
-                    if (!string.IsNullOrEmpty(Language))
-                    {
-                        attributes.Add(StringHelper.FirstToUpper(Language));
-                    }
-                    else
-                    {
-                        attributes.Add(string.IsNullOrEmpty(localizedUndefined) ? "Und" : localizedUndefined);
-                    }
-
-                    if (IsDefault)
-                    {
-                        attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault);
-                    }
-
-                    if (IsForced)
-                    {
-                        attributes.Add(string.IsNullOrEmpty(localizedForced) ? "Forced" : localizedForced);
-                    }
-
-                    if (!string.IsNullOrEmpty(Title))
-                    {
-                        return attributes.AsEnumerable()
-                        // keep Tags that are not already in Title
-                        .Where(tag => Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1)
-                        // attributes concatenation, starting with Title
-                        .Aggregate(new StringBuilder(Title), (builder, attr) => builder.Append(" - ").Append(attr))
-                        .ToString();
-                    }
-
-                    return string.Join(" - ", attributes.ToArray());
-                }
-
-                if (Type == MediaStreamType.Video)
-                {
-                }
-
-                return null;
             }
         }
 

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

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

+ 2 - 2
MediaBrowser.Providers/Manager/ProviderManager.cs

@@ -265,7 +265,7 @@ namespace MediaBrowser.Providers.Manager
             }
             catch (Exception ex)
             {
-                _logger.LogError(ex, "{0} failed in GetImageInfos for type {1}", provider.GetType().Name, item.GetType().Name);
+                _logger.LogError(ex, "{ProviderName} failed in GetImageInfos for type {ItemType} at {ItemPath}", provider.GetType().Name, item.GetType().Name, item.Path);
                 return new List<RemoteImageInfo>();
             }
         }
@@ -430,7 +430,7 @@ namespace MediaBrowser.Providers.Manager
             }
             catch (Exception ex)
             {
-                _logger.LogError(ex, "{0} failed in Supports for type {1}", provider.GetType().Name, item.GetType().Name);
+                _logger.LogError(ex, "{ProviderName} failed in Supports for type {ItemType} at {ItemPath}", provider.GetType().Name, item.GetType().Name, item.Path);
                 return false;
             }
         }

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

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

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

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

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

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

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

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

+ 2 - 2
debian/conf/jellyfin

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