Sfoglia il codice sorgente

Merge branch 'master' into usings

Bond-009 5 anni fa
parent
commit
f31efce52d
67 ha cambiato i file con 533 aggiunte e 312 eliminazioni
  1. 0 1
      Emby.Notifications/Api/NotificationsService.cs
  2. 0 1
      Emby.Notifications/CoreNotificationTypes.cs
  3. 0 1
      Emby.Notifications/NotificationConfigurationFactory.cs
  4. 2 5
      Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
  5. 21 33
      Emby.Server.Implementations/ApplicationHost.cs
  6. 16 19
      Emby.Server.Implementations/Browser/BrowserLauncher.cs
  7. 11 2
      Emby.Server.Implementations/ConfigurationOptions.cs
  8. 7 4
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  9. 10 5
      Emby.Server.Implementations/EntryPoints/StartupWizard.cs
  10. 11 2
      Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
  11. 0 1
      Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
  12. 0 1
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  13. 0 1
      Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs
  14. 0 1
      Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
  15. 0 1
      Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
  16. 0 1
      Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
  17. 0 1
      Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
  18. 0 1
      Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
  19. 0 1
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  20. 0 1
      Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
  21. 0 1
      Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs
  22. 0 1
      Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
  23. 0 1
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  24. 0 1
      Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
  25. 0 1
      Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
  26. 0 1
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
  27. 0 1
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
  28. 0 1
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
  29. 0 1
      Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
  30. 0 1
      Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
  31. 0 1
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  32. 0 1
      Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
  33. 25 3
      Emby.Server.Implementations/Localization/Core/bg-BG.json
  34. 23 1
      Emby.Server.Implementations/Localization/Core/fr.json
  35. 25 3
      Emby.Server.Implementations/Localization/Core/it.json
  36. 23 1
      Emby.Server.Implementations/Localization/Core/pt-BR.json
  37. 20 20
      Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
  38. 49 14
      Jellyfin.Server/Program.cs
  39. 11 0
      Jellyfin.Server/Properties/launchSettings.json
  40. 25 0
      Jellyfin.Server/StartupOptions.cs
  41. 7 2
      MediaBrowser.Common/Configuration/IApplicationPaths.cs
  42. 3 2
      MediaBrowser.Controller/Chapters/IChapterManager.cs
  43. 2 3
      MediaBrowser.Controller/Entities/Folder.cs
  44. 1 1
      MediaBrowser.Controller/Entities/IItemByName.cs
  45. 15 0
      MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
  46. 5 0
      MediaBrowser.Controller/IServerApplicationHost.cs
  47. 0 1
      MediaBrowser.Controller/Library/IMediaSourceProvider.cs
  48. 3 1
      MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs
  49. 1 1
      MediaBrowser.Controller/Persistence/IItemRepository.cs
  50. 3 5
      MediaBrowser.Controller/Providers/DirectoryService.cs
  51. 2 2
      MediaBrowser.Controller/Providers/IDirectoryService.cs
  52. 1 1
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  53. 2 5
      MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
  54. 2 2
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  55. 6 16
      MediaBrowser.Providers/Chapters/ChapterManager.cs
  56. 37 45
      MediaBrowser.Providers/Manager/ProviderManager.cs
  57. 12 0
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  58. 13 1
      MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
  59. 75 59
      MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
  60. 7 7
      MediaBrowser.Providers/Tmdb/Movies/TmdbMovieProvider.cs
  61. 36 10
      MediaBrowser.WebDashboard/Api/DashboardService.cs
  62. 16 6
      MediaBrowser.WebDashboard/Api/PackageCreator.cs
  63. 1 1
      tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
  64. 1 1
      tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
  65. 1 1
      tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
  66. 1 1
      tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
  67. 1 1
      tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj

+ 0 - 1
Emby.Notifications/Api/NotificationsService.cs

@@ -1,6 +1,5 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 #pragma warning disable SA1402
 #pragma warning disable SA1402
-#pragma warning disable SA1600
 #pragma warning disable SA1649
 #pragma warning disable SA1649
 
 
 using System;
 using System;

+ 0 - 1
Emby.Notifications/CoreNotificationTypes.cs

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;

+ 0 - 1
Emby.Notifications/NotificationConfigurationFactory.cs

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System.Collections.Generic;
 using System.Collections.Generic;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;

+ 2 - 5
Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs

@@ -5,7 +5,7 @@ using MediaBrowser.Common.Configuration;
 namespace Emby.Server.Implementations.AppBase
 namespace Emby.Server.Implementations.AppBase
 {
 {
     /// <summary>
     /// <summary>
-    /// Provides a base class to hold common application paths used by both the Ui and Server.
+    /// Provides a base class to hold common application paths used by both the UI and Server.
     /// This can be subclassed to add application-specific paths.
     /// This can be subclassed to add application-specific paths.
     /// </summary>
     /// </summary>
     public abstract class BaseApplicationPaths : IApplicationPaths
     public abstract class BaseApplicationPaths : IApplicationPaths
@@ -37,10 +37,7 @@ namespace Emby.Server.Implementations.AppBase
         /// <value>The program data path.</value>
         /// <value>The program data path.</value>
         public string ProgramDataPath { get; }
         public string ProgramDataPath { get; }
 
 
-        /// <summary>
-        /// Gets the path to the web UI resources folder.
-        /// </summary>
-        /// <value>The web UI resources path.</value>
+        /// <inheritdoc/>
         public string WebPath { get; }
         public string WebPath { get; }
 
 
         /// <summary>
         /// <summary>

+ 21 - 33
Emby.Server.Implementations/ApplicationHost.cs

@@ -235,11 +235,6 @@ namespace Emby.Server.Implementations
         /// </summary>
         /// </summary>
         public int HttpsPort { get; private set; }
         public int HttpsPort { get; private set; }
 
 
-        /// <summary>
-        /// Gets the content root for the webhost.
-        /// </summary>
-        public string ContentRoot { get; private set; }
-
         /// <summary>
         /// <summary>
         /// Gets the server configuration manager.
         /// Gets the server configuration manager.
         /// </summary>
         /// </summary>
@@ -612,13 +607,7 @@ namespace Emby.Server.Implementations
 
 
             DiscoverTypes();
             DiscoverTypes();
 
 
-            await RegisterResources(serviceCollection, startupConfig).ConfigureAwait(false);
-
-            ContentRoot = ServerConfigurationManager.Configuration.DashboardSourcePath;
-            if (string.IsNullOrEmpty(ContentRoot))
-            {
-                ContentRoot = ServerConfigurationManager.ApplicationPaths.WebPath;
-            }
+            await RegisterServices(serviceCollection, startupConfig).ConfigureAwait(false);
         }
         }
 
 
         public async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next)
         public async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next)
@@ -649,9 +638,9 @@ namespace Emby.Server.Implementations
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Registers resources that classes will depend on
+        /// Registers services/resources with the service collection that will be available via DI.
         /// </summary>
         /// </summary>
-        protected async Task RegisterResources(IServiceCollection serviceCollection, IConfiguration startupConfig)
+        protected async Task RegisterServices(IServiceCollection serviceCollection, IConfiguration startupConfig)
         {
         {
             serviceCollection.AddMemoryCache();
             serviceCollection.AddMemoryCache();
 
 
@@ -769,20 +758,8 @@ namespace Emby.Server.Implementations
             CertificateInfo = GetCertificateInfo(true);
             CertificateInfo = GetCertificateInfo(true);
             Certificate = GetCertificate(CertificateInfo);
             Certificate = GetCertificate(CertificateInfo);
 
 
-            HttpServer = new HttpListenerHost(
-                this,
-                LoggerFactory.CreateLogger<HttpListenerHost>(),
-                ServerConfigurationManager,
-                startupConfig,
-                NetworkManager,
-                JsonSerializer,
-                XmlSerializer,
-                CreateHttpListener())
-            {
-                GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading")
-            };
-
-            serviceCollection.AddSingleton(HttpServer);
+            serviceCollection.AddSingleton<IHttpListener, WebSocketSharpListener>();
+            serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
 
 
             ImageProcessor = new ImageProcessor(LoggerFactory.CreateLogger<ImageProcessor>(), ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder);
             ImageProcessor = new ImageProcessor(LoggerFactory.CreateLogger<ImageProcessor>(), ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder);
             serviceCollection.AddSingleton(ImageProcessor);
             serviceCollection.AddSingleton(ImageProcessor);
@@ -844,10 +821,15 @@ namespace Emby.Server.Implementations
 
 
             serviceCollection.AddSingleton<IDeviceDiscovery>(new DeviceDiscovery(ServerConfigurationManager));
             serviceCollection.AddSingleton<IDeviceDiscovery>(new DeviceDiscovery(ServerConfigurationManager));
 
 
-            ChapterManager = new ChapterManager(LibraryManager, LoggerFactory, ServerConfigurationManager, ItemRepository);
+            ChapterManager = new ChapterManager(ItemRepository);
             serviceCollection.AddSingleton(ChapterManager);
             serviceCollection.AddSingleton(ChapterManager);
 
 
-            EncodingManager = new MediaEncoder.EncodingManager(FileSystemManager, LoggerFactory, MediaEncoder, ChapterManager, LibraryManager);
+            EncodingManager = new MediaEncoder.EncodingManager(
+                LoggerFactory.CreateLogger<MediaEncoder.EncodingManager>(),
+                FileSystemManager,
+                MediaEncoder,
+                ChapterManager,
+                LibraryManager);
             serviceCollection.AddSingleton(EncodingManager);
             serviceCollection.AddSingleton(EncodingManager);
 
 
             var activityLogRepo = GetActivityLogRepository();
             var activityLogRepo = GetActivityLogRepository();
@@ -890,6 +872,14 @@ namespace Emby.Server.Implementations
             ((LibraryManager)LibraryManager).ItemRepository = ItemRepository;
             ((LibraryManager)LibraryManager).ItemRepository = ItemRepository;
         }
         }
 
 
+        /// <summary>
+        /// Create services registered with the service container that need to be initialized at application startup.
+        /// </summary>
+        public void InitializeServices()
+        {
+            HttpServer = Resolve<IHttpServer>();
+        }
+
         public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths)
         public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths)
         {
         {
             // Distinct these to prevent users from reporting problems that aren't actually problems
             // Distinct these to prevent users from reporting problems that aren't actually problems
@@ -1167,7 +1157,7 @@ namespace Emby.Server.Implementations
                 {
                 {
                     exportedTypes = ass.GetExportedTypes();
                     exportedTypes = ass.GetExportedTypes();
                 }
                 }
-                catch (TypeLoadException ex)
+                catch (FileNotFoundException ex)
                 {
                 {
                     Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
                     Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
                     continue;
                     continue;
@@ -1207,8 +1197,6 @@ namespace Emby.Server.Implementations
             });
             });
         }
         }
 
 
-        protected IHttpListener CreateHttpListener() => new WebSocketSharpListener(LoggerFactory.CreateLogger<WebSocketSharpListener>());
-
         private CertificateInfo GetCertificateInfo(bool generateCertificate)
         private CertificateInfo GetCertificateInfo(bool generateCertificate)
         {
         {
             // Custom cert
             // Custom cert

+ 16 - 19
Emby.Server.Implementations/Browser/BrowserLauncher.cs

@@ -1,51 +1,48 @@
 using System;
 using System;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
+using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.Browser
 namespace Emby.Server.Implementations.Browser
 {
 {
     /// <summary>
     /// <summary>
-    /// Class BrowserLauncher.
+    /// Assists in opening application URLs in an external browser.
     /// </summary>
     /// </summary>
     public static class BrowserLauncher
     public static class BrowserLauncher
     {
     {
         /// <summary>
         /// <summary>
-        /// Opens the dashboard page.
+        /// Opens the home page of the web client.
         /// </summary>
         /// </summary>
-        /// <param name="page">The page.</param>
         /// <param name="appHost">The app host.</param>
         /// <param name="appHost">The app host.</param>
-        private static void OpenDashboardPage(string page, IServerApplicationHost appHost)
+        public static void OpenWebApp(IServerApplicationHost appHost)
         {
         {
-            var url = appHost.GetLocalApiUrl("localhost") + "/web/" + page;
-
-            OpenUrl(appHost, url);
+            TryOpenUrl(appHost, "/web/index.html");
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Opens the web client.
+        /// Opens the swagger API page.
         /// </summary>
         /// </summary>
         /// <param name="appHost">The app host.</param>
         /// <param name="appHost">The app host.</param>
-        public static void OpenWebApp(IServerApplicationHost appHost)
+        public static void OpenSwaggerPage(IServerApplicationHost appHost)
         {
         {
-            OpenDashboardPage("index.html", appHost);
+            TryOpenUrl(appHost, "/swagger/index.html");
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Opens the URL.
+        /// Opens the specified URL in an external browser window. Any exceptions will be logged, but ignored.
         /// </summary>
         /// </summary>
-        /// <param name="appHost">The application host instance.</param>
+        /// <param name="appHost">The application host.</param>
         /// <param name="url">The URL.</param>
         /// <param name="url">The URL.</param>
-        private static void OpenUrl(IServerApplicationHost appHost, string url)
+        private static void TryOpenUrl(IServerApplicationHost appHost, string url)
         {
         {
             try
             try
             {
             {
-                appHost.LaunchUrl(url);
-            }
-            catch (NotSupportedException)
-            {
-
+                string baseUrl = appHost.GetLocalApiUrl("localhost");
+                appHost.LaunchUrl(baseUrl + url);
             }
             }
-            catch (Exception)
+            catch (Exception ex)
             {
             {
+                var logger = appHost.Resolve<ILogger>();
+                logger?.LogError(ex, "Failed to open browser window with URL {URL}", url);
             }
             }
         }
         }
     }
     }

+ 11 - 2
Emby.Server.Implementations/ConfigurationOptions.cs

@@ -1,13 +1,22 @@
 using System.Collections.Generic;
 using System.Collections.Generic;
+using Emby.Server.Implementations.HttpServer;
+using MediaBrowser.Providers.Music;
 using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
 using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
 
 
 namespace Emby.Server.Implementations
 namespace Emby.Server.Implementations
 {
 {
+    /// <summary>
+    /// Static class containing the default configuration options for the web server.
+    /// </summary>
     public static class ConfigurationOptions
     public static class ConfigurationOptions
     {
     {
-        public static Dictionary<string, string> Configuration => new Dictionary<string, string>
+        /// <summary>
+        /// Gets a new copy of the default configuration options.
+        /// </summary>
+        public static Dictionary<string, string> DefaultConfiguration => new Dictionary<string, string>
         {
         {
-            { "HttpListenerHost:DefaultRedirectPath", "web/index.html" },
+            { HostWebClientKey, bool.TrueString },
+            { HttpListenerHost.DefaultRedirectKey, "web/index.html" },
             { FfmpegProbeSizeKey, "1G" },
             { FfmpegProbeSizeKey, "1G" },
             { FfmpegAnalyzeDurationKey, "200M" },
             { FfmpegAnalyzeDurationKey, "200M" },
             { PlaylistsAllowDuplicatesKey, bool.TrueString }
             { PlaylistsAllowDuplicatesKey, bool.TrueString }

+ 7 - 4
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -2006,7 +2006,7 @@ namespace Emby.Server.Implementations.Data
         /// <summary>
         /// <summary>
         /// Saves the chapters.
         /// Saves the chapters.
         /// </summary>
         /// </summary>
-        public void SaveChapters(Guid id, List<ChapterInfo> chapters)
+        public void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters)
         {
         {
             CheckDisposed();
             CheckDisposed();
 
 
@@ -2035,22 +2035,24 @@ namespace Emby.Server.Implementations.Data
             }
             }
         }
         }
 
 
-        private void InsertChapters(byte[] idBlob, List<ChapterInfo> chapters, IDatabaseConnection db)
+        private void InsertChapters(byte[] idBlob, IReadOnlyList<ChapterInfo> chapters, IDatabaseConnection db)
         {
         {
             var startIndex = 0;
             var startIndex = 0;
             var limit = 100;
             var limit = 100;
             var chapterIndex = 0;
             var chapterIndex = 0;
 
 
+            const string StartInsertText = "insert into " + ChaptersTableName + " (ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath, ImageDateModified) values ";
+            var insertText = new StringBuilder(StartInsertText, 256);
+
             while (startIndex < chapters.Count)
             while (startIndex < chapters.Count)
             {
             {
-                var insertText = new StringBuilder("insert into " + ChaptersTableName + " (ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath, ImageDateModified) values ");
-
                 var endIndex = Math.Min(chapters.Count, startIndex + limit);
                 var endIndex = Math.Min(chapters.Count, startIndex + limit);
 
 
                 for (var i = startIndex; i < endIndex; i++)
                 for (var i = startIndex; i < endIndex; i++)
                 {
                 {
                     insertText.AppendFormat("(@ItemId, @ChapterIndex{0}, @StartPositionTicks{0}, @Name{0}, @ImagePath{0}, @ImageDateModified{0}),", i.ToString(CultureInfo.InvariantCulture));
                     insertText.AppendFormat("(@ItemId, @ChapterIndex{0}, @StartPositionTicks{0}, @Name{0}, @ImagePath{0}, @ImageDateModified{0}),", i.ToString(CultureInfo.InvariantCulture));
                 }
                 }
+
                 insertText.Length -= 1; // Remove last ,
                 insertText.Length -= 1; // Remove last ,
 
 
                 using (var statement = PrepareStatement(db, insertText.ToString()))
                 using (var statement = PrepareStatement(db, insertText.ToString()))
@@ -2077,6 +2079,7 @@ namespace Emby.Server.Implementations.Data
                 }
                 }
 
 
                 startIndex += limit;
                 startIndex += limit;
+                insertText.Length = StartInsertText.Length;
             }
             }
         }
         }
 
 

+ 10 - 5
Emby.Server.Implementations/EntryPoints/StartupWizard.cs

@@ -2,7 +2,9 @@ using System.Threading.Tasks;
 using Emby.Server.Implementations.Browser;
 using Emby.Server.Implementations.Browser;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Plugins;
+using Microsoft.Extensions.Configuration;
 
 
 namespace Emby.Server.Implementations.EntryPoints
 namespace Emby.Server.Implementations.EntryPoints
 {
 {
@@ -11,10 +13,8 @@ namespace Emby.Server.Implementations.EntryPoints
     /// </summary>
     /// </summary>
     public sealed class StartupWizard : IServerEntryPoint
     public sealed class StartupWizard : IServerEntryPoint
     {
     {
-        /// <summary>
-        /// The app host.
-        /// </summary>
         private readonly IServerApplicationHost _appHost;
         private readonly IServerApplicationHost _appHost;
+        private readonly IConfiguration _appConfig;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
 
 
         /// <summary>
         /// <summary>
@@ -22,9 +22,10 @@ namespace Emby.Server.Implementations.EntryPoints
         /// </summary>
         /// </summary>
         /// <param name="appHost">The application host.</param>
         /// <param name="appHost">The application host.</param>
         /// <param name="config">The configuration manager.</param>
         /// <param name="config">The configuration manager.</param>
-        public StartupWizard(IServerApplicationHost appHost, IServerConfigurationManager config)
+        public StartupWizard(IServerApplicationHost appHost, IConfiguration appConfig, IServerConfigurationManager config)
         {
         {
             _appHost = appHost;
             _appHost = appHost;
+            _appConfig = appConfig;
             _config = config;
             _config = config;
         }
         }
 
 
@@ -36,7 +37,11 @@ namespace Emby.Server.Implementations.EntryPoints
                 return Task.CompletedTask;
                 return Task.CompletedTask;
             }
             }
 
 
-            if (!_config.Configuration.IsStartupWizardCompleted)
+            if (!_appConfig.HostWebClient())
+            {
+                BrowserLauncher.OpenSwaggerPage(_appHost);
+            }
+            else if (!_config.Configuration.IsStartupWizardCompleted)
             {
             {
                 BrowserLauncher.OpenWebApp(_appHost);
                 BrowserLauncher.OpenWebApp(_appHost);
             }
             }

+ 11 - 2
Emby.Server.Implementations/HttpServer/HttpListenerHost.cs

@@ -17,6 +17,7 @@ using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Services;
 using MediaBrowser.Model.Services;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
@@ -29,6 +30,12 @@ namespace Emby.Server.Implementations.HttpServer
 {
 {
     public class HttpListenerHost : IHttpServer, IDisposable
     public class HttpListenerHost : IHttpServer, IDisposable
     {
     {
+        /// <summary>
+        /// The key for a setting that specifies the default redirect path
+        /// to use for requests where the URL base prefix is invalid or missing.
+        /// </summary>
+        public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath";
+
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
         private readonly INetworkManager _networkManager;
         private readonly INetworkManager _networkManager;
@@ -52,12 +59,13 @@ namespace Emby.Server.Implementations.HttpServer
             INetworkManager networkManager,
             INetworkManager networkManager,
             IJsonSerializer jsonSerializer,
             IJsonSerializer jsonSerializer,
             IXmlSerializer xmlSerializer,
             IXmlSerializer xmlSerializer,
-            IHttpListener socketListener)
+            IHttpListener socketListener,
+            ILocalizationManager localizationManager)
         {
         {
             _appHost = applicationHost;
             _appHost = applicationHost;
             _logger = logger;
             _logger = logger;
             _config = config;
             _config = config;
-            _defaultRedirectPath = configuration["HttpListenerHost:DefaultRedirectPath"];
+            _defaultRedirectPath = configuration[DefaultRedirectKey];
             _baseUrlPrefix = _config.Configuration.BaseUrl;
             _baseUrlPrefix = _config.Configuration.BaseUrl;
             _networkManager = networkManager;
             _networkManager = networkManager;
             _jsonSerializer = jsonSerializer;
             _jsonSerializer = jsonSerializer;
@@ -69,6 +77,7 @@ namespace Emby.Server.Implementations.HttpServer
 
 
             Instance = this;
             Instance = this;
             ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>();
             ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>();
+            GlobalResponse = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
         }
         }
 
 
         public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
         public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;

+ 0 - 1
Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System;
 using System;
 using System.IO;
 using System.IO;

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

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;

+ 0 - 1
Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Plugins;

+ 0 - 1
Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System;
 using System;
 using System.Threading;
 using System.Threading;

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

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;

+ 0 - 1
Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System;
 using System;
 using System.Globalization;
 using System.Globalization;

+ 0 - 1
Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System;
 using System;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;

+ 0 - 1
Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System;
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Concurrent;

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

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System;
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Concurrent;

+ 0 - 1
Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;

+ 0 - 1
Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System.Collections.Generic;
 using System.Collections.Generic;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;

+ 0 - 1
Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System;
 using System;
 using System.Globalization;
 using System.Globalization;

+ 0 - 1
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;

+ 0 - 1
Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;

+ 0 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System;
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Concurrent;

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

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;

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

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System;
 using System;
 using System.Buffers;
 using System.Buffers;

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

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;

+ 0 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;

+ 0 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;

+ 0 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;

+ 0 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;

+ 25 - 3
Emby.Server.Implementations/Localization/Core/bg-BG.json

@@ -1,8 +1,8 @@
 {
 {
     "Albums": "Албуми",
     "Albums": "Албуми",
-    "AppDeviceValues": "Програма: {0}, устройство: {1}",
+    "AppDeviceValues": "Програма: {0}, Устройство: {1}",
     "Application": "Програма",
     "Application": "Програма",
-    "Artists": "Изпълнители",
+    "Artists": "Артисти",
     "AuthenticationSucceededWithUserName": "{0} се удостовери успешно",
     "AuthenticationSucceededWithUserName": "{0} се удостовери успешно",
     "Books": "Книги",
     "Books": "Книги",
     "CameraImageUploadedFrom": "Нова снимка от камера беше качена от {0}",
     "CameraImageUploadedFrom": "Нова снимка от камера беше качена от {0}",
@@ -92,5 +92,27 @@
     "UserStoppedPlayingItemWithValues": "{0} спря {1}",
     "UserStoppedPlayingItemWithValues": "{0} спря {1}",
     "ValueHasBeenAddedToLibrary": "{0} беше добавен във Вашата библиотека",
     "ValueHasBeenAddedToLibrary": "{0} беше добавен във Вашата библиотека",
     "ValueSpecialEpisodeName": "Специални - {0}",
     "ValueSpecialEpisodeName": "Специални - {0}",
-    "VersionNumber": "Версия {0}"
+    "VersionNumber": "Версия {0}",
+    "TaskDownloadMissingSubtitlesDescription": "Търси Интернет за липсващи поднадписи, на база конфигурацията за мета-данни.",
+    "TaskDownloadMissingSubtitles": "Изтегляне на липсващи поднадписи",
+    "TaskRefreshChannelsDescription": "Обновява информацията за интернет канала.",
+    "TaskRefreshChannels": "Обновяване на Канали",
+    "TaskCleanTranscodeDescription": "Изтрива прекодирани файлове по-стари от един ден.",
+    "TaskCleanTranscode": "Изчиства директорията за прекодиране",
+    "TaskUpdatePluginsDescription": "Изтегля и инсталира актуализации за добавките, които са настроени за автоматична актуализация.",
+    "TaskUpdatePlugins": "Актуализира добавките",
+    "TaskRefreshPeopleDescription": "Актуализира мета-данните за артистите и режисьорите за Вашата медийна библиотека.",
+    "TaskRefreshPeople": "Обновяване на участниците",
+    "TaskCleanLogsDescription": "Изтрива лог файлове по-стари от {0} дни.",
+    "TaskCleanLogs": "Изчисти директорията с логове",
+    "TaskRefreshLibraryDescription": "Сканира Вашата библиотека с медия за нови файлове и обновява мета-данните.",
+    "TaskRefreshLibrary": "Сканиране на библиотеката с медия",
+    "TaskRefreshChapterImagesDescription": "Създава иконки за видеа, които имат епизоди.",
+    "TaskRefreshChapterImages": "Извличане на изображения за епизода",
+    "TaskCleanCacheDescription": "Изтриване на ненужните от системата файлове.",
+    "TaskCleanCache": "Изчистване на Кеш-директорията",
+    "TasksChannelsCategory": "Интернет Канали",
+    "TasksApplicationCategory": "Приложение",
+    "TasksLibraryCategory": "Библиотека",
+    "TasksMaintenanceCategory": "Поддръжка"
 }
 }

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

@@ -92,5 +92,27 @@
     "UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}",
     "UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}",
     "ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque",
     "ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque",
     "ValueSpecialEpisodeName": "Spécial - {0}",
     "ValueSpecialEpisodeName": "Spécial - {0}",
-    "VersionNumber": "Version {0}"
+    "VersionNumber": "Version {0}",
+    "TasksChannelsCategory": "Chaines en ligne",
+    "TaskDownloadMissingSubtitlesDescription": "Cherche les sous-titres manquant sur internet en se basant sur la configuration des métadonnées.",
+    "TaskDownloadMissingSubtitles": "Télécharge les sous-titres manquant",
+    "TaskRefreshChannelsDescription": "Rafraîchit les informations des chaines en ligne.",
+    "TaskRefreshChannels": "Rafraîchit les chaines",
+    "TaskCleanTranscodeDescription": "Supprime les fichiers transcodés de plus d'un jour.",
+    "TaskCleanTranscode": "Nettoie les dossier des transcodages",
+    "TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des plugins configurés pour être mis à jour automatiquement.",
+    "TaskUpdatePlugins": "Mettre à jour les plugins",
+    "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et directeurs dans votre bibliothèque.",
+    "TaskRefreshPeople": "Rafraîchit les acteurs",
+    "TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
+    "TaskCleanLogs": "Nettoie le répertoire des journaux",
+    "TaskRefreshLibraryDescription": "Scanne toute les bibliothèques pour trouver les nouveaux fichiers et rafraîchit les métadonnées.",
+    "TaskRefreshLibrary": "Scanne toute les Bibliothèques",
+    "TaskRefreshChapterImagesDescription": "Crée des images de miniature pour les vidéos ayant des chapitres.",
+    "TaskRefreshChapterImages": "Extrait les images de chapitre",
+    "TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
+    "TaskCleanCache": "Vider le répertoire cache",
+    "TasksApplicationCategory": "Application",
+    "TasksLibraryCategory": "Bibliothèque",
+    "TasksMaintenanceCategory": "Maintenance"
 }
 }

+ 25 - 3
Emby.Server.Implementations/Localization/Core/it.json

@@ -5,7 +5,7 @@
     "Artists": "Artisti",
     "Artists": "Artisti",
     "AuthenticationSucceededWithUserName": "{0} autenticato con successo",
     "AuthenticationSucceededWithUserName": "{0} autenticato con successo",
     "Books": "Libri",
     "Books": "Libri",
-    "CameraImageUploadedFrom": "È stata caricata una nuova immagine della fotocamera da {0}",
+    "CameraImageUploadedFrom": "È stata caricata una nuova immagine della fotocamera dal device {0}",
     "Channels": "Canali",
     "Channels": "Canali",
     "ChapterNameValue": "Capitolo {0}",
     "ChapterNameValue": "Capitolo {0}",
     "Collections": "Collezioni",
     "Collections": "Collezioni",
@@ -15,7 +15,7 @@
     "Favorites": "Preferiti",
     "Favorites": "Preferiti",
     "Folders": "Cartelle",
     "Folders": "Cartelle",
     "Genres": "Generi",
     "Genres": "Generi",
-    "HeaderAlbumArtists": "Artisti dell' Album",
+    "HeaderAlbumArtists": "Artisti degli Album",
     "HeaderCameraUploads": "Caricamenti Fotocamera",
     "HeaderCameraUploads": "Caricamenti Fotocamera",
     "HeaderContinueWatching": "Continua a guardare",
     "HeaderContinueWatching": "Continua a guardare",
     "HeaderFavoriteAlbums": "Album Preferiti",
     "HeaderFavoriteAlbums": "Album Preferiti",
@@ -92,5 +92,27 @@
     "UserStoppedPlayingItemWithValues": "{0} ha interrotto la riproduzione di {1} su {2}",
     "UserStoppedPlayingItemWithValues": "{0} ha interrotto la riproduzione di {1} su {2}",
     "ValueHasBeenAddedToLibrary": "{0} è stato aggiunto alla tua libreria multimediale",
     "ValueHasBeenAddedToLibrary": "{0} è stato aggiunto alla tua libreria multimediale",
     "ValueSpecialEpisodeName": "Speciale - {0}",
     "ValueSpecialEpisodeName": "Speciale - {0}",
-    "VersionNumber": "Versione {0}"
+    "VersionNumber": "Versione {0}",
+    "TaskRefreshChannelsDescription": "Aggiorna le informazioni dei canali Internet.",
+    "TaskDownloadMissingSubtitlesDescription": "Cerca su internet i sottotitoli mancanti basandosi sulle configurazioni dei metadati.",
+    "TaskDownloadMissingSubtitles": "Scarica i sottotitoli mancanti",
+    "TaskRefreshChannels": "Aggiorna i canali",
+    "TaskCleanTranscodeDescription": "Cancella i file di transcode più vecchi di un giorno.",
+    "TaskCleanTranscode": "Svuota la cartella del transcoding",
+    "TaskUpdatePluginsDescription": "Scarica e installa gli aggiornamenti per i plugin che sono stati configurati per essere aggiornati contemporaneamente.",
+    "TaskUpdatePlugins": "Aggiorna i Plugin",
+    "TaskRefreshPeopleDescription": "Aggiorna i metadati per gli attori e registi nella tua libreria multimediale.",
+    "TaskRefreshPeople": "Aggiorna persone",
+    "TaskCleanLogsDescription": "Rimuovi i file di log più vecchi di {0} giorni.",
+    "TaskCleanLogs": "Pulisci la cartella dei log",
+    "TaskRefreshLibraryDescription": "Analizza la tua libreria multimediale per nuovi file e rinnova i metadati.",
+    "TaskRefreshLibrary": "Analizza la libreria dei contenuti multimediali",
+    "TaskRefreshChapterImagesDescription": "Crea le thumbnail per i video che hanno capitoli.",
+    "TaskRefreshChapterImages": "Estrai immagini capitolo",
+    "TaskCleanCacheDescription": "Cancella i file di cache non più necessari al sistema.",
+    "TaskCleanCache": "Pulisci la directory della cache",
+    "TasksChannelsCategory": "Canali su Internet",
+    "TasksApplicationCategory": "Applicazione",
+    "TasksLibraryCategory": "Libreria",
+    "TasksMaintenanceCategory": "Manutenzione"
 }
 }

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

@@ -92,5 +92,27 @@
     "UserStoppedPlayingItemWithValues": "{0} parou de reproduzir {1} em {2}",
     "UserStoppedPlayingItemWithValues": "{0} parou de reproduzir {1} em {2}",
     "ValueHasBeenAddedToLibrary": "{0} foi adicionado à sua biblioteca de mídia",
     "ValueHasBeenAddedToLibrary": "{0} foi adicionado à sua biblioteca de mídia",
     "ValueSpecialEpisodeName": "Especial - {0}",
     "ValueSpecialEpisodeName": "Especial - {0}",
-    "VersionNumber": "Versão {0}"
+    "VersionNumber": "Versão {0}",
+    "TaskDownloadMissingSubtitlesDescription": "Procurar na internet por legendas faltando baseado na configuração de metadados.",
+    "TaskDownloadMissingSubtitles": "Baixar legendas que estão faltando",
+    "TaskRefreshChannelsDescription": "Atualizar informação de canais da internet .",
+    "TaskRefreshChannels": "Atualizar Canais",
+    "TaskCleanTranscodeDescription": "Deletar arquivos de transcodificação com mais de um dia de criação.",
+    "TaskCleanTranscode": "Limpar pasta de transcodificação",
+    "TaskUpdatePluginsDescription": "Baixa e instala atualizações para plugins que estão configurados para atualizar automaticamente.",
+    "TaskUpdatePlugins": "Atualizar Plugins",
+    "TaskRefreshPeopleDescription": "Atualiza metadados para atores e diretores na sua biblioteca de mídia.",
+    "TaskRefreshPeople": "Atualizar pessoas",
+    "TaskCleanLogsDescription": "Deletar arquivos temporários com mais de {0} dias.",
+    "TaskCleanLogs": "Limpar pasta de logs",
+    "TaskRefreshLibraryDescription": "Escaneie a sua biblioteca de mídia para arquivos novos e atualize os metadados.",
+    "TaskRefreshLibrary": "Escanear a Biblioteca de Mídia",
+    "TaskRefreshChapterImagesDescription": "Criar miniaturas para vídeos que tem capítulos.",
+    "TaskRefreshChapterImages": "Extrair imagens dos capítulos",
+    "TaskCleanCacheDescription": "Deletar arquivos temporários que não são mais necessários para o sistema.",
+    "TaskCleanCache": "Limpar Arquivos Temporários",
+    "TasksChannelsCategory": "Canais da Internet",
+    "TasksApplicationCategory": "Aplicativo",
+    "TasksLibraryCategory": "Biblioteca",
+    "TasksMaintenanceCategory": "Manutenção"
 }
 }

+ 20 - 20
Emby.Server.Implementations/MediaEncoder/EncodingManager.cs

@@ -26,14 +26,20 @@ namespace Emby.Server.Implementations.MediaEncoder
         private readonly IChapterManager _chapterManager;
         private readonly IChapterManager _chapterManager;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
 
 
+        /// <summary>
+        /// The first chapter ticks.
+        /// </summary>
+        private static readonly long _firstChapterTicks = TimeSpan.FromSeconds(15).Ticks;
+
         public EncodingManager(
         public EncodingManager(
+            ILogger<EncodingManager> logger,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
-            ILoggerFactory loggerFactory,
             IMediaEncoder encoder,
             IMediaEncoder encoder,
-            IChapterManager chapterManager, ILibraryManager libraryManager)
+            IChapterManager chapterManager,
+            ILibraryManager libraryManager)
         {
         {
+            _logger = logger;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
-            _logger = loggerFactory.CreateLogger(nameof(EncodingManager));
             _encoder = encoder;
             _encoder = encoder;
             _chapterManager = chapterManager;
             _chapterManager = chapterManager;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
@@ -97,12 +103,7 @@ namespace Emby.Server.Implementations.MediaEncoder
             return video.DefaultVideoStreamIndex.HasValue;
             return video.DefaultVideoStreamIndex.HasValue;
         }
         }
 
 
-        /// <summary>
-        /// The first chapter ticks
-        /// </summary>
-        private static readonly long FirstChapterTicks = TimeSpan.FromSeconds(15).Ticks;
-
-        public async Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, List<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken)
+        public async Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken)
         {
         {
             if (!IsEligibleForChapterImageExtraction(video))
             if (!IsEligibleForChapterImageExtraction(video))
             {
             {
@@ -135,7 +136,7 @@ namespace Emby.Server.Implementations.MediaEncoder
                         try
                         try
                         {
                         {
                             // Add some time for the first chapter to make sure we don't end up with a black image
                             // Add some time for the first chapter to make sure we don't end up with a black image
-                            var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(FirstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks);
+                            var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(_firstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks);
 
 
                             var protocol = MediaProtocol.File;
                             var protocol = MediaProtocol.File;
 
 
@@ -152,9 +153,9 @@ namespace Emby.Server.Implementations.MediaEncoder
                             {
                             {
                                 _fileSystem.DeleteFile(tempFile);
                                 _fileSystem.DeleteFile(tempFile);
                             }
                             }
-                            catch
+                            catch (IOException ex)
                             {
                             {
-
+                                _logger.LogError(ex, "Error deleting temporary chapter image encoding file {Path}", tempFile);
                             }
                             }
 
 
                             chapter.ImagePath = path;
                             chapter.ImagePath = path;
@@ -184,7 +185,7 @@ namespace Emby.Server.Implementations.MediaEncoder
 
 
             if (saveChapters && changesMade)
             if (saveChapters && changesMade)
             {
             {
-                _chapterManager.SaveChapters(video.Id.ToString(), chapters);
+                _chapterManager.SaveChapters(video.Id, chapters);
             }
             }
 
 
             DeleteDeadImages(currentImages, chapters);
             DeleteDeadImages(currentImages, chapters);
@@ -199,22 +200,21 @@ namespace Emby.Server.Implementations.MediaEncoder
             return Path.Combine(GetChapterImagesPath(video), filename);
             return Path.Combine(GetChapterImagesPath(video), filename);
         }
         }
 
 
-        private static List<string> GetSavedChapterImages(Video video, IDirectoryService directoryService)
+        private static IReadOnlyList<string> GetSavedChapterImages(Video video, IDirectoryService directoryService)
         {
         {
             var path = GetChapterImagesPath(video);
             var path = GetChapterImagesPath(video);
             if (!Directory.Exists(path))
             if (!Directory.Exists(path))
             {
             {
-                return new List<string>();
+                return Array.Empty<string>();
             }
             }
 
 
             try
             try
             {
             {
-                return directoryService.GetFilePaths(path)
-                    .ToList();
+                return directoryService.GetFilePaths(path);
             }
             }
             catch (IOException)
             catch (IOException)
             {
             {
-                return new List<string>();
+                return Array.Empty<string>();
             }
             }
         }
         }
 
 
@@ -227,7 +227,7 @@ namespace Emby.Server.Implementations.MediaEncoder
 
 
             foreach (var image in deadImages)
             foreach (var image in deadImages)
             {
             {
-                _logger.LogDebug("Deleting dead chapter image {path}", image);
+                _logger.LogDebug("Deleting dead chapter image {Path}", image);
 
 
                 try
                 try
                 {
                 {
@@ -235,7 +235,7 @@ namespace Emby.Server.Implementations.MediaEncoder
                 }
                 }
                 catch (IOException ex)
                 catch (IOException ex)
                 {
                 {
-                    _logger.LogError(ex, "Error deleting {path}.", image);
+                    _logger.LogError(ex, "Error deleting {Path}.", image);
                 }
                 }
             }
             }
         }
         }

+ 49 - 14
Jellyfin.Server/Program.cs

@@ -12,11 +12,14 @@ using System.Threading.Tasks;
 using CommandLine;
 using CommandLine;
 using Emby.Drawing;
 using Emby.Drawing;
 using Emby.Server.Implementations;
 using Emby.Server.Implementations;
+using Emby.Server.Implementations.HttpServer;
 using Emby.Server.Implementations.IO;
 using Emby.Server.Implementations.IO;
 using Emby.Server.Implementations.Networking;
 using Emby.Server.Implementations.Networking;
 using Jellyfin.Drawing.Skia;
 using Jellyfin.Drawing.Skia;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Extensions;
+using MediaBrowser.WebDashboard.Api;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
@@ -109,9 +112,10 @@ namespace Jellyfin.Server
             // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
             // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
             Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath);
             Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath);
 
 
-            // Create an instance of the application configuration to use for application startup
             await InitLoggingConfigFile(appPaths).ConfigureAwait(false);
             await InitLoggingConfigFile(appPaths).ConfigureAwait(false);
-            IConfiguration startupConfig = CreateAppConfiguration(appPaths);
+
+            // Create an instance of the application configuration to use for application startup
+            IConfiguration startupConfig = CreateAppConfiguration(options, appPaths);
 
 
             // Initialize logging framework
             // Initialize logging framework
             InitializeLoggingFramework(startupConfig, appPaths);
             InitializeLoggingFramework(startupConfig, appPaths);
@@ -180,15 +184,31 @@ namespace Jellyfin.Server
                 new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
                 new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
                 GetImageEncoder(appPaths),
                 GetImageEncoder(appPaths),
                 new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()));
                 new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()));
+
             try
             try
             {
             {
+                // If hosting the web client, validate the client content path
+                if (startupConfig.HostWebClient())
+                {
+                    string webContentPath = DashboardService.GetDashboardUIPath(startupConfig, appHost.ServerConfigurationManager);
+                    if (!Directory.Exists(webContentPath) || Directory.GetFiles(webContentPath).Length == 0)
+                    {
+                        throw new InvalidOperationException(
+                            "The server is expected to host the web client, but the provided content directory is either " +
+                            $"invalid or empty: {webContentPath}. If you do not want to host the web client with the " +
+                            "server, you may set the '--nowebclient' command line flag, or set" +
+                            $"'{MediaBrowser.Controller.Extensions.ConfigurationExtensions.HostWebClientKey}=false' in your config settings.");
+                    }
+                }
+
                 ServiceCollection serviceCollection = new ServiceCollection();
                 ServiceCollection serviceCollection = new ServiceCollection();
                 await appHost.InitAsync(serviceCollection, startupConfig).ConfigureAwait(false);
                 await appHost.InitAsync(serviceCollection, startupConfig).ConfigureAwait(false);
 
 
-                var webHost = CreateWebHostBuilder(appHost, serviceCollection, appPaths).Build();
+                var webHost = CreateWebHostBuilder(appHost, serviceCollection, options, startupConfig, appPaths).Build();
 
 
-                // A bit hacky to re-use service provider since ASP.NET doesn't allow a custom service collection.
+                // Re-use the web host service provider in the app host since ASP.NET doesn't allow a custom service collection.
                 appHost.ServiceProvider = webHost.Services;
                 appHost.ServiceProvider = webHost.Services;
+                appHost.InitializeServices();
                 appHost.FindParts();
                 appHost.FindParts();
                 Migrations.MigrationRunner.Run(appHost, _loggerFactory);
                 Migrations.MigrationRunner.Run(appHost, _loggerFactory);
 
 
@@ -230,7 +250,12 @@ namespace Jellyfin.Server
             }
             }
         }
         }
 
 
-        private static IWebHostBuilder CreateWebHostBuilder(ApplicationHost appHost, IServiceCollection serviceCollection, IApplicationPaths appPaths)
+        private static IWebHostBuilder CreateWebHostBuilder(
+            ApplicationHost appHost,
+            IServiceCollection serviceCollection,
+            StartupOptions commandLineOpts,
+            IConfiguration startupConfig,
+            IApplicationPaths appPaths)
         {
         {
             return new WebHostBuilder()
             return new WebHostBuilder()
                 .UseKestrel(options =>
                 .UseKestrel(options =>
@@ -270,9 +295,8 @@ namespace Jellyfin.Server
                         }
                         }
                     }
                     }
                 })
                 })
-                .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(appPaths))
+                .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(commandLineOpts, appPaths, startupConfig))
                 .UseSerilog()
                 .UseSerilog()
-                .UseContentRoot(appHost.ContentRoot)
                 .ConfigureServices(services =>
                 .ConfigureServices(services =>
                 {
                 {
                     // Merge the external ServiceCollection into ASP.NET DI
                     // Merge the external ServiceCollection into ASP.NET DI
@@ -395,9 +419,8 @@ namespace Jellyfin.Server
             // webDir
             // webDir
             // IF      --webdir
             // IF      --webdir
             // ELSE IF $JELLYFIN_WEB_DIR
             // ELSE IF $JELLYFIN_WEB_DIR
-            // ELSE    use <bindir>/jellyfin-web
+            // ELSE    <bindir>/jellyfin-web
             var webDir = options.WebDir;
             var webDir = options.WebDir;
-
             if (string.IsNullOrEmpty(webDir))
             if (string.IsNullOrEmpty(webDir))
             {
             {
                 webDir = Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR");
                 webDir = Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR");
@@ -468,21 +491,33 @@ namespace Jellyfin.Server
             await resource.CopyToAsync(dst).ConfigureAwait(false);
             await resource.CopyToAsync(dst).ConfigureAwait(false);
         }
         }
 
 
-        private static IConfiguration CreateAppConfiguration(IApplicationPaths appPaths)
+        private static IConfiguration CreateAppConfiguration(StartupOptions commandLineOpts, IApplicationPaths appPaths)
         {
         {
             return new ConfigurationBuilder()
             return new ConfigurationBuilder()
-                .ConfigureAppConfiguration(appPaths)
+                .ConfigureAppConfiguration(commandLineOpts, appPaths)
                 .Build();
                 .Build();
         }
         }
 
 
-        private static IConfigurationBuilder ConfigureAppConfiguration(this IConfigurationBuilder config, IApplicationPaths appPaths)
+        private static IConfigurationBuilder ConfigureAppConfiguration(
+            this IConfigurationBuilder config,
+            StartupOptions commandLineOpts,
+            IApplicationPaths appPaths,
+            IConfiguration? startupConfig = null)
         {
         {
+            // Use the swagger API page as the default redirect path if not hosting the web client
+            var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration;
+            if (startupConfig != null && !startupConfig.HostWebClient())
+            {
+                inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "swagger/index.html";
+            }
+
             return config
             return config
                 .SetBasePath(appPaths.ConfigurationDirectoryPath)
                 .SetBasePath(appPaths.ConfigurationDirectoryPath)
-                .AddInMemoryCollection(ConfigurationOptions.Configuration)
+                .AddInMemoryCollection(inMemoryDefaultConfig)
                 .AddJsonFile(LoggingConfigFileDefault, optional: false, reloadOnChange: true)
                 .AddJsonFile(LoggingConfigFileDefault, optional: false, reloadOnChange: true)
                 .AddJsonFile(LoggingConfigFileSystem, optional: true, reloadOnChange: true)
                 .AddJsonFile(LoggingConfigFileSystem, optional: true, reloadOnChange: true)
-                .AddEnvironmentVariables("JELLYFIN_");
+                .AddEnvironmentVariables("JELLYFIN_")
+                .AddInMemoryCollection(commandLineOpts.ConvertToConfig());
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 11 - 0
Jellyfin.Server/Properties/launchSettings.json

@@ -0,0 +1,11 @@
+{
+  "profiles": {
+    "Jellyfin.Server": {
+      "commandName": "Project"
+    },
+    "Jellyfin.Server (nowebclient)": {
+      "commandName": "Project",
+      "commandLineArgs": "--nowebclient"
+    }
+  }
+}

+ 25 - 0
Jellyfin.Server/StartupOptions.cs

@@ -1,5 +1,8 @@
+using System.Collections.Generic;
+using System.Globalization;
 using CommandLine;
 using CommandLine;
 using Emby.Server.Implementations;
 using Emby.Server.Implementations;
+using MediaBrowser.Controller.Extensions;
 
 
 namespace Jellyfin.Server
 namespace Jellyfin.Server
 {
 {
@@ -15,6 +18,12 @@ namespace Jellyfin.Server
         [Option('d', "datadir", Required = false, HelpText = "Path to use for the data folder (database files, etc.).")]
         [Option('d', "datadir", Required = false, HelpText = "Path to use for the data folder (database files, etc.).")]
         public string? DataDir { get; set; }
         public string? DataDir { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets a value indicating whether the server should not host the web client.
+        /// </summary>
+        [Option("nowebclient", Required = false, HelpText = "Indicates that the web server should not host the web client.")]
+        public bool NoWebClient { get; set; }
+
         /// <summary>
         /// <summary>
         /// Gets or sets the path to the web directory.
         /// Gets or sets the path to the web directory.
         /// </summary>
         /// </summary>
@@ -66,5 +75,21 @@ namespace Jellyfin.Server
         /// <inheritdoc />
         /// <inheritdoc />
         [Option("restartargs", Required = false, HelpText = "Arguments for restart script.")]
         [Option("restartargs", Required = false, HelpText = "Arguments for restart script.")]
         public string? RestartArgs { get; set; }
         public string? RestartArgs { get; set; }
+
+        /// <summary>
+        /// Gets the command line options as a dictionary that can be used in the .NET configuration system.
+        /// </summary>
+        /// <returns>The configuration dictionary.</returns>
+        public Dictionary<string, string> ConvertToConfig()
+        {
+            var config = new Dictionary<string, string>();
+
+            if (NoWebClient)
+            {
+                config.Add(ConfigurationExtensions.HostWebClientKey, bool.FalseString);
+            }
+
+            return config;
+        }
     }
     }
 }
 }

+ 7 - 2
MediaBrowser.Common/Configuration/IApplicationPaths.cs

@@ -1,3 +1,5 @@
+using MediaBrowser.Model.Configuration;
+
 namespace MediaBrowser.Common.Configuration
 namespace MediaBrowser.Common.Configuration
 {
 {
     /// <summary>
     /// <summary>
@@ -12,9 +14,12 @@ namespace MediaBrowser.Common.Configuration
         string ProgramDataPath { get; }
         string ProgramDataPath { get; }
 
 
         /// <summary>
         /// <summary>
-        /// Gets the path to the web UI resources folder
+        /// Gets the path to the web UI resources folder.
         /// </summary>
         /// </summary>
-        /// <value>The web UI resources path.</value>
+        /// <remarks>
+        /// This value is not relevant if the server is configured to not host any static web content. Additionally,
+        /// the value for <see cref="ServerConfiguration.DashboardSourcePath"/> takes precedence over this one.
+        /// </remarks>
         string WebPath { get; }
         string WebPath { get; }
 
 
         /// <summary>
         /// <summary>

+ 3 - 2
MediaBrowser.Controller/Chapters/IChapterManager.cs

@@ -1,16 +1,17 @@
+using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 
 
 namespace MediaBrowser.Controller.Chapters
 namespace MediaBrowser.Controller.Chapters
 {
 {
     /// <summary>
     /// <summary>
-    /// Interface IChapterManager
+    /// Interface IChapterManager.
     /// </summary>
     /// </summary>
     public interface IChapterManager
     public interface IChapterManager
     {
     {
         /// <summary>
         /// <summary>
         /// Saves the chapters.
         /// Saves the chapters.
         /// </summary>
         /// </summary>
-        void SaveChapters(string itemId, List<ChapterInfo> chapters);
+        void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters);
     }
     }
 }
 }

+ 2 - 3
MediaBrowser.Controller/Entities/Folder.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
@@ -28,7 +30,6 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     /// </summary>
     public class Folder : BaseItem
     public class Folder : BaseItem
     {
     {
-        public static IUserManager UserManager { get; set; }
         public static IUserViewManager UserViewManager { get; set; }
         public static IUserViewManager UserViewManager { get; set; }
 
 
         /// <summary>
         /// <summary>
@@ -620,7 +621,6 @@ namespace MediaBrowser.Controller.Entities
                 {
                 {
                     EnableImages = false
                     EnableImages = false
                 }
                 }
-
             }).TotalRecordCount;
             }).TotalRecordCount;
         }
         }
 
 
@@ -1713,7 +1713,6 @@ namespace MediaBrowser.Controller.Entities
                     {
                     {
                         EnableImages = false
                         EnableImages = false
                     }
                     }
-
                 });
                 });
 
 
                 double unplayedCount = unplayedQueryResult.TotalRecordCount;
                 double unplayedCount = unplayedQueryResult.TotalRecordCount;

+ 1 - 1
MediaBrowser.Controller/Entities/IItemByName.cs

@@ -3,7 +3,7 @@ using System.Collections.Generic;
 namespace MediaBrowser.Controller.Entities
 namespace MediaBrowser.Controller.Entities
 {
 {
     /// <summary>
     /// <summary>
-    /// Marker interface
+    /// Marker interface.
     /// </summary>
     /// </summary>
     public interface IItemByName
     public interface IItemByName
     {
     {

+ 15 - 0
MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs

@@ -1,3 +1,4 @@
+using System;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Configuration;
 
 
 namespace MediaBrowser.Controller.Extensions
 namespace MediaBrowser.Controller.Extensions
@@ -7,6 +8,11 @@ namespace MediaBrowser.Controller.Extensions
     /// </summary>
     /// </summary>
     public static class ConfigurationExtensions
     public static class ConfigurationExtensions
     {
     {
+        /// <summary>
+        /// The key for a setting that indicates whether the application should host web client content.
+        /// </summary>
+        public const string HostWebClientKey = "hostwebclient";
+
         /// <summary>
         /// <summary>
         /// The key for the FFmpeg probe size option.
         /// The key for the FFmpeg probe size option.
         /// </summary>
         /// </summary>
@@ -22,6 +28,15 @@ namespace MediaBrowser.Controller.Extensions
         /// </summary>
         /// </summary>
         public const string PlaylistsAllowDuplicatesKey = "playlists:allowDuplicates";
         public const string PlaylistsAllowDuplicatesKey = "playlists:allowDuplicates";
 
 
+        /// <summary>
+        /// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>.
+        /// </summary>
+        /// <param name="configuration">The configuration to retrieve the value from.</param>
+        /// <returns>The parsed config value.</returns>
+        /// <exception cref="FormatException">The config value is not a valid bool string. See <see cref="bool.Parse(string)"/>.</exception>
+        public static bool HostWebClient(this IConfiguration configuration)
+            => configuration.GetValue<bool>(HostWebClientKey);
+
         /// <summary>
         /// <summary>
         /// Gets the FFmpeg probe size from the <see cref="IConfiguration" />.
         /// Gets the FFmpeg probe size from the <see cref="IConfiguration" />.
         /// </summary>
         /// </summary>

+ 5 - 0
MediaBrowser.Controller/IServerApplicationHost.cs

@@ -82,6 +82,11 @@ namespace MediaBrowser.Controller
         /// <returns>The local API URL.</returns>
         /// <returns>The local API URL.</returns>
         string GetLocalApiUrl(IPAddress address);
         string GetLocalApiUrl(IPAddress address);
 
 
+        /// <summary>
+        /// Open a URL in an external browser window.
+        /// </summary>
+        /// <param name="url">The URL to open.</param>
+        /// <exception cref="NotSupportedException"><see cref="CanLaunchWebBrowser"/> is false.</exception>
         void LaunchUrl(string url);
         void LaunchUrl(string url);
 
 
         void EnableLoopback(string appName);
         void EnableLoopback(string appName);

+ 0 - 1
MediaBrowser.Controller/Library/IMediaSourceProvider.cs

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-#pragma warning disable SA1600
 
 
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Threading;
 using System.Threading;

+ 3 - 1
MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
@@ -12,6 +14,6 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// <summary>
         /// <summary>
         /// Refreshes the chapter images.
         /// Refreshes the chapter images.
         /// </summary>
         /// </summary>
-        Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, List<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken);
+        Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken);
     }
     }
 }
 }

+ 1 - 1
MediaBrowser.Controller/Persistence/IItemRepository.cs

@@ -61,7 +61,7 @@ namespace MediaBrowser.Controller.Persistence
         /// <summary>
         /// <summary>
         /// Saves the chapters.
         /// Saves the chapters.
         /// </summary>
         /// </summary>
-        void SaveChapters(Guid id, List<ChapterInfo> chapters);
+        void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters);
 
 
         /// <summary>
         /// <summary>
         /// Gets the media streams.
         /// Gets the media streams.

+ 3 - 5
MediaBrowser.Controller/Providers/DirectoryService.cs

@@ -66,12 +66,10 @@ namespace MediaBrowser.Controller.Providers
             return file;
             return file;
         }
         }
 
 
-        public List<string> GetFilePaths(string path)
-        {
-            return GetFilePaths(path, false);
-        }
+        public IReadOnlyList<string> GetFilePaths(string path)
+            => GetFilePaths(path, false);
 
 
-        public List<string> GetFilePaths(string path, bool clearCache)
+        public IReadOnlyList<string> GetFilePaths(string path, bool clearCache)
         {
         {
             if (clearCache || !_filePathCache.TryGetValue(path, out List<string> result))
             if (clearCache || !_filePathCache.TryGetValue(path, out List<string> result))
             {
             {

+ 2 - 2
MediaBrowser.Controller/Providers/IDirectoryService.cs

@@ -11,8 +11,8 @@ namespace MediaBrowser.Controller.Providers
 
 
         FileSystemMetadata GetFile(string path);
         FileSystemMetadata GetFile(string path);
 
 
-        List<string> GetFilePaths(string path);
+        IReadOnlyList<string> GetFilePaths(string path);
 
 
-        List<string> GetFilePaths(string path, bool clearCache);
+        IReadOnlyList<string> GetFilePaths(string path, bool clearCache);
     }
     }
 }
 }

+ 1 - 1
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -429,7 +429,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
                     }
                     }
                 }
                 }
 
 
-                return new ProbeResultNormalizer(_logger, _fileSystem, _localization).GetMediaInfo(result, videoType, isAudio, primaryPath, protocol);
+                return new ProbeResultNormalizer(_logger, _localization).GetMediaInfo(result, videoType, isAudio, primaryPath, protocol);
             }
             }
         }
         }
 
 

+ 2 - 5
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs

@@ -9,7 +9,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
@@ -19,13 +18,11 @@ namespace MediaBrowser.MediaEncoding.Probing
     {
     {
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly ILogger _logger;
         private readonly ILogger _logger;
-        private readonly IFileSystem _fileSystem;
         private readonly ILocalizationManager _localization;
         private readonly ILocalizationManager _localization;
 
 
-        public ProbeResultNormalizer(ILogger logger, IFileSystem fileSystem, ILocalizationManager localization)
+        public ProbeResultNormalizer(ILogger logger, ILocalizationManager localization)
         {
         {
             _logger = logger;
             _logger = logger;
-            _fileSystem = fileSystem;
             _localization = localization;
             _localization = localization;
         }
         }
 
 
@@ -40,7 +37,7 @@ namespace MediaBrowser.MediaEncoding.Probing
             FFProbeHelpers.NormalizeFFProbeResult(data);
             FFProbeHelpers.NormalizeFFProbeResult(data);
             SetSize(data, info);
             SetSize(data, info);
 
 
-            var internalStreams = data.Streams ?? new MediaStreamInfo[] { };
+            var internalStreams = data.Streams ?? Array.Empty<MediaStreamInfo>();
 
 
             info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, s, data.Format))
             info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, s, data.Format))
                 .Where(i => i != null)
                 .Where(i => i != null)

+ 2 - 2
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -148,9 +148,9 @@ namespace MediaBrowser.Model.Configuration
         public bool EnableDashboardResponseCaching { get; set; }
         public bool EnableDashboardResponseCaching { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Allows the dashboard to be served from a custom path.
+        /// Gets or sets a custom path to serve the dashboard from.
         /// </summary>
         /// </summary>
-        /// <value>The dashboard source path.</value>
+        /// <value>The dashboard source path, or null if the default path should be used.</value>
         public string DashboardSourcePath { get; set; }
         public string DashboardSourcePath { get; set; }
 
 
         /// <summary>
         /// <summary>

+ 6 - 16
MediaBrowser.Providers/Chapters/ChapterManager.cs

@@ -1,36 +1,26 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using MediaBrowser.Controller.Chapters;
 using MediaBrowser.Controller.Chapters;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
-using Microsoft.Extensions.Logging;
 
 
 namespace MediaBrowser.Providers.Chapters
 namespace MediaBrowser.Providers.Chapters
 {
 {
     public class ChapterManager : IChapterManager
     public class ChapterManager : IChapterManager
     {
     {
-        private readonly ILibraryManager _libraryManager;
-        private readonly ILogger _logger;
-        private readonly IServerConfigurationManager _config;
         private readonly IItemRepository _itemRepo;
         private readonly IItemRepository _itemRepo;
 
 
-        public ChapterManager(
-            ILibraryManager libraryManager,
-            ILoggerFactory loggerFactory,
-            IServerConfigurationManager config,
-            IItemRepository itemRepo)
+        public ChapterManager(IItemRepository itemRepo)
         {
         {
-            _libraryManager = libraryManager;
-            _logger = loggerFactory.CreateLogger(nameof(ChapterManager));
-            _config = config;
             _itemRepo = itemRepo;
             _itemRepo = itemRepo;
         }
         }
 
 
-        public void SaveChapters(string itemId, List<ChapterInfo> chapters)
+        /// <inheritdoc />
+        public void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters)
         {
         {
-            _itemRepo.SaveChapters(new Guid(itemId), chapters);
+            _itemRepo.SaveChapters(itemId, chapters);
         }
         }
     }
     }
 }
 }

+ 37 - 45
MediaBrowser.Providers/Manager/ProviderManager.cs

@@ -1,5 +1,9 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
@@ -897,7 +901,10 @@ namespace MediaBrowser.Providers.Manager
                 return new ExternalUrl
                 return new ExternalUrl
                 {
                 {
                     Name = i.Name,
                     Name = i.Name,
-                    Url = string.Format(i.UrlFormatString, value)
+                    Url = string.Format(
+                        CultureInfo.InvariantCulture,
+                        i.UrlFormatString,
+                        value)
                 };
                 };
 
 
             }).Where(i => i != null).Concat(item.GetRelatedUrls());
             }).Where(i => i != null).Concat(item.GetRelatedUrls());
@@ -911,11 +918,10 @@ namespace MediaBrowser.Providers.Manager
                     Name = i.Name,
                     Name = i.Name,
                     Key = i.Key,
                     Key = i.Key,
                     UrlFormatString = i.UrlFormatString
                     UrlFormatString = i.UrlFormatString
-
                 });
                 });
         }
         }
 
 
-        private Dictionary<Guid, double> _activeRefreshes = new Dictionary<Guid, double>();
+        private ConcurrentDictionary<Guid, double> _activeRefreshes = new ConcurrentDictionary<Guid, double>();
 
 
         public Dictionary<Guid, Guid> GetRefreshQueue()
         public Dictionary<Guid, Guid> GetRefreshQueue()
         {
         {
@@ -927,66 +933,54 @@ namespace MediaBrowser.Providers.Manager
                 {
                 {
                     dict[item.Item1] = item.Item1;
                     dict[item.Item1] = item.Item1;
                 }
                 }
+
                 return dict;
                 return dict;
             }
             }
         }
         }
 
 
         public void OnRefreshStart(BaseItem item)
         public void OnRefreshStart(BaseItem item)
         {
         {
-            //_logger.LogInformation("OnRefreshStart {0}", item.Id.ToString("N", CultureInfo.InvariantCulture));
-            var id = item.Id;
-
-            lock (_activeRefreshes)
-            {
-                _activeRefreshes[id] = 0;
-            }
-
+            _logger.LogInformation("OnRefreshStart {0}", item.Id.ToString("N", CultureInfo.InvariantCulture));
+            _activeRefreshes[item.Id] = 0;
             RefreshStarted?.Invoke(this, new GenericEventArgs<BaseItem>(item));
             RefreshStarted?.Invoke(this, new GenericEventArgs<BaseItem>(item));
         }
         }
 
 
         public void OnRefreshComplete(BaseItem item)
         public void OnRefreshComplete(BaseItem item)
         {
         {
-            //_logger.LogInformation("OnRefreshComplete {0}", item.Id.ToString("N", CultureInfo.InvariantCulture));
-            lock (_activeRefreshes)
-            {
-                _activeRefreshes.Remove(item.Id);
-            }
+            _logger.LogInformation("OnRefreshComplete {0}", item.Id.ToString("N", CultureInfo.InvariantCulture));
+
+            _activeRefreshes.Remove(item.Id, out _);
 
 
             RefreshCompleted?.Invoke(this, new GenericEventArgs<BaseItem>(item));
             RefreshCompleted?.Invoke(this, new GenericEventArgs<BaseItem>(item));
         }
         }
 
 
         public double? GetRefreshProgress(Guid id)
         public double? GetRefreshProgress(Guid id)
         {
         {
-            lock (_activeRefreshes)
+            if (_activeRefreshes.TryGetValue(id, out double value))
             {
             {
-                if (_activeRefreshes.TryGetValue(id, out double value))
-                {
-                    return value;
-                }
-
-                return null;
+                return value;
             }
             }
+
+            return null;
         }
         }
 
 
         public void OnRefreshProgress(BaseItem item, double progress)
         public void OnRefreshProgress(BaseItem item, double progress)
         {
         {
-            //_logger.LogInformation("OnRefreshProgress {0} {1}", item.Id.ToString("N", CultureInfo.InvariantCulture), progress);
             var id = item.Id;
             var id = item.Id;
-
-            lock (_activeRefreshes)
-            {
-                if (_activeRefreshes.ContainsKey(id))
-                {
-                    _activeRefreshes[id] = progress;
-
-                    RefreshProgress?.Invoke(this, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(item, progress)));
-                }
-                else
-                {
-                    // TODO: Need to hunt down the conditions for this happening
-                    //throw new Exception(string.Format("Refresh for item {0} {1} is not in progress", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture)));
-                }
-            }
+            _logger.LogInformation("OnRefreshProgress {0} {1}", id.ToString("N", CultureInfo.InvariantCulture), progress);
+
+            // TODO: Need to hunt down the conditions for this happening
+            _activeRefreshes.AddOrUpdate(
+                id,
+                (_) => throw new Exception(
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        "Cannot update refresh progress of item '{0}' ({1}) because a refresh for this item is not running",
+                        item.GetType().Name,
+                        item.Id.ToString("N", CultureInfo.InvariantCulture))),
+                (_, __) => progress);
+
+            RefreshProgress?.Invoke(this, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(item, progress)));
         }
         }
 
 
         private readonly SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>> _refreshQueue =
         private readonly SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>> _refreshQueue =
@@ -1040,10 +1034,9 @@ namespace MediaBrowser.Providers.Manager
                         // Try to throttle this a little bit.
                         // Try to throttle this a little bit.
                         await Task.Delay(100).ConfigureAwait(false);
                         await Task.Delay(100).ConfigureAwait(false);
 
 
-                        var artist = item as MusicArtist;
-                        var task = artist == null
-                            ? RefreshItem(item, refreshItem.Item2, cancellationToken)
-                            : RefreshArtist(artist, refreshItem.Item2, cancellationToken);
+                        var task = item is MusicArtist artist
+                            ? RefreshArtist(artist, refreshItem.Item2, cancellationToken)
+                            : RefreshItem(item, refreshItem.Item2, cancellationToken);
 
 
                         await task.ConfigureAwait(false);
                         await task.ConfigureAwait(false);
                     }
                     }
@@ -1125,8 +1118,7 @@ namespace MediaBrowser.Providers.Manager
             }
             }
         }
         }
 
 
-        public Task RefreshFullItem(BaseItem item, MetadataRefreshOptions options,
-            CancellationToken cancellationToken)
+        public Task RefreshFullItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
         {
             return RefreshItem(item, options, cancellationToken);
             return RefreshItem(item, options, cancellationToken);
         }
         }

+ 12 - 0
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -24,6 +24,18 @@
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>
   </PropertyGroup>
 
 
+  <!-- Code Analyzers-->
+  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
+    <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+    <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+  </ItemGroup>
+
+  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
+  </PropertyGroup>
+
   <ItemGroup>
   <ItemGroup>
     <None Remove="Plugins\AudioDb\Configuration\config.html" />
     <None Remove="Plugins\AudioDb\Configuration\config.html" />
     <EmbeddedResource Include="Plugins\AudioDb\Configuration\config.html" />
     <EmbeddedResource Include="Plugins\AudioDb\Configuration\config.html" />

+ 13 - 1
MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs

@@ -194,7 +194,19 @@ namespace MediaBrowser.Providers.MediaInfo
                 FetchShortcutInfo(item);
                 FetchShortcutInfo(item);
             }
             }
 
 
-            var prober = new FFProbeVideoInfo(_logger, _mediaSourceManager, _isoManager, _mediaEncoder, _itemRepo, _blurayExaminer, _localization, _appPaths, _json, _encodingManager, _fileSystem, _config, _subtitleManager, _chapterManager, _libraryManager);
+            var prober = new FFProbeVideoInfo(
+                _logger,
+                _mediaSourceManager,
+                _mediaEncoder,
+                _itemRepo,
+                _blurayExaminer,
+                _localization,
+                _encodingManager,
+                _fileSystem,
+                _config,
+                _subtitleManager,
+                _chapterManager,
+                _libraryManager);
 
 
             return prober.ProbeVideo(item, options, cancellationToken);
             return prober.ProbeVideo(item, options, cancellationToken);
         }
         }

+ 75 - 59
MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
@@ -25,7 +27,6 @@ using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
 namespace MediaBrowser.Providers.MediaInfo
 namespace MediaBrowser.Providers.MediaInfo
@@ -33,13 +34,10 @@ namespace MediaBrowser.Providers.MediaInfo
     public class FFProbeVideoInfo
     public class FFProbeVideoInfo
     {
     {
         private readonly ILogger _logger;
         private readonly ILogger _logger;
-        private readonly IIsoManager _isoManager;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IItemRepository _itemRepo;
         private readonly IItemRepository _itemRepo;
         private readonly IBlurayExaminer _blurayExaminer;
         private readonly IBlurayExaminer _blurayExaminer;
         private readonly ILocalizationManager _localization;
         private readonly ILocalizationManager _localization;
-        private readonly IApplicationPaths _appPaths;
-        private readonly IJsonSerializer _json;
         private readonly IEncodingManager _encodingManager;
         private readonly IEncodingManager _encodingManager;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
@@ -48,16 +46,27 @@ namespace MediaBrowser.Providers.MediaInfo
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IMediaSourceManager _mediaSourceManager;
 
 
-        public FFProbeVideoInfo(ILogger logger, IMediaSourceManager mediaSourceManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization, IApplicationPaths appPaths, IJsonSerializer json, IEncodingManager encodingManager, IFileSystem fileSystem, IServerConfigurationManager config, ISubtitleManager subtitleManager, IChapterManager chapterManager, ILibraryManager libraryManager)
+        private readonly long _dummyChapterDuration = TimeSpan.FromMinutes(5).Ticks;
+
+        public FFProbeVideoInfo(
+            ILogger logger,
+            IMediaSourceManager mediaSourceManager,
+            IMediaEncoder mediaEncoder,
+            IItemRepository itemRepo,
+            IBlurayExaminer blurayExaminer,
+            ILocalizationManager localization,
+            IEncodingManager encodingManager,
+            IFileSystem fileSystem,
+            IServerConfigurationManager config,
+            ISubtitleManager subtitleManager,
+            IChapterManager chapterManager,
+            ILibraryManager libraryManager)
         {
         {
             _logger = logger;
             _logger = logger;
-            _isoManager = isoManager;
             _mediaEncoder = mediaEncoder;
             _mediaEncoder = mediaEncoder;
             _itemRepo = itemRepo;
             _itemRepo = itemRepo;
             _blurayExaminer = blurayExaminer;
             _blurayExaminer = blurayExaminer;
             _localization = localization;
             _localization = localization;
-            _appPaths = appPaths;
-            _json = json;
             _encodingManager = encodingManager;
             _encodingManager = encodingManager;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
             _config = config;
             _config = config;
@@ -159,7 +168,7 @@ namespace MediaBrowser.Providers.MediaInfo
         {
         {
             List<MediaStream> mediaStreams;
             List<MediaStream> mediaStreams;
             IReadOnlyList<MediaAttachment> mediaAttachments;
             IReadOnlyList<MediaAttachment> mediaAttachments;
-            List<ChapterInfo> chapters;
+            ChapterInfo[] chapters;
 
 
             if (mediaInfo != null)
             if (mediaInfo != null)
             {
             {
@@ -177,6 +186,7 @@ namespace MediaBrowser.Providers.MediaInfo
                 {
                 {
                     video.RunTimeTicks = mediaInfo.RunTimeTicks;
                     video.RunTimeTicks = mediaInfo.RunTimeTicks;
                 }
                 }
+
                 video.Size = mediaInfo.Size;
                 video.Size = mediaInfo.Size;
 
 
                 if (video.VideoType == VideoType.VideoFile)
                 if (video.VideoType == VideoType.VideoFile)
@@ -189,19 +199,20 @@ namespace MediaBrowser.Providers.MediaInfo
                 {
                 {
                     video.Container = null;
                     video.Container = null;
                 }
                 }
+
                 video.Container = mediaInfo.Container;
                 video.Container = mediaInfo.Container;
 
 
-                chapters = mediaInfo.Chapters == null ? new List<ChapterInfo>() : mediaInfo.Chapters.ToList();
+                chapters = mediaInfo.Chapters == null ? Array.Empty<ChapterInfo>() : mediaInfo.Chapters;
                 if (blurayInfo != null)
                 if (blurayInfo != null)
                 {
                 {
-                    FetchBdInfo(video, chapters, mediaStreams, blurayInfo);
+                    FetchBdInfo(video, ref chapters, mediaStreams, blurayInfo);
                 }
                 }
             }
             }
             else
             else
             {
             {
                 mediaStreams = new List<MediaStream>();
                 mediaStreams = new List<MediaStream>();
                 mediaAttachments = Array.Empty<MediaAttachment>();
                 mediaAttachments = Array.Empty<MediaAttachment>();
-                chapters = new List<ChapterInfo>();
+                chapters = Array.Empty<ChapterInfo>();
             }
             }
 
 
             await AddExternalSubtitles(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
             await AddExternalSubtitles(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
@@ -231,9 +242,9 @@ namespace MediaBrowser.Providers.MediaInfo
             if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh ||
             if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh ||
                 options.MetadataRefreshMode == MetadataRefreshMode.Default)
                 options.MetadataRefreshMode == MetadataRefreshMode.Default)
             {
             {
-                if (chapters.Count == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video))
+                if (chapters.Length == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video))
                 {
                 {
-                    AddDummyChapters(video, chapters);
+                    chapters = CreateDummyChapters(video);
                 }
                 }
 
 
                 NormalizeChapterNames(chapters);
                 NormalizeChapterNames(chapters);
@@ -246,28 +257,29 @@ namespace MediaBrowser.Providers.MediaInfo
 
 
                 await _encodingManager.RefreshChapterImages(video, options.DirectoryService, chapters, extractDuringScan, false, cancellationToken).ConfigureAwait(false);
                 await _encodingManager.RefreshChapterImages(video, options.DirectoryService, chapters, extractDuringScan, false, cancellationToken).ConfigureAwait(false);
 
 
-                _chapterManager.SaveChapters(video.Id.ToString(), chapters);
+                _chapterManager.SaveChapters(video.Id, chapters);
             }
             }
         }
         }
 
 
-        private void NormalizeChapterNames(List<ChapterInfo> chapters)
+        private void NormalizeChapterNames(ChapterInfo[] chapters)
         {
         {
-            var index = 1;
-
-            foreach (var chapter in chapters)
+            for (int i = 0; i < chapters.Length; i++)
             {
             {
+                string name = chapters[i].Name;
                 // Check if the name is empty and/or if the name is a time
                 // Check if the name is empty and/or if the name is a time
                 // Some ripping programs do that.
                 // Some ripping programs do that.
-                if (string.IsNullOrWhiteSpace(chapter.Name) ||
-                    TimeSpan.TryParse(chapter.Name, out var time))
+                if (string.IsNullOrWhiteSpace(name) ||
+                    TimeSpan.TryParse(name, out _))
                 {
                 {
-                    chapter.Name = string.Format(_localization.GetLocalizedString("ChapterNameValue"), index.ToString(CultureInfo.InvariantCulture));
+                    chapters[i].Name = string.Format(
+                        CultureInfo.InvariantCulture,
+                        _localization.GetLocalizedString("ChapterNameValue"),
+                        (i + 1).ToString(CultureInfo.InvariantCulture));
                 }
                 }
-                index++;
             }
             }
         }
         }
 
 
-        private void FetchBdInfo(BaseItem item, List<ChapterInfo> chapters, List<MediaStream> mediaStreams, BlurayDiscInfo blurayInfo)
+        private void FetchBdInfo(BaseItem item, ref ChapterInfo[] chapters, List<MediaStream> mediaStreams, BlurayDiscInfo blurayInfo)
         {
         {
             var video = (Video)item;
             var video = (Video)item;
 
 
@@ -301,13 +313,15 @@ namespace MediaBrowser.Providers.MediaInfo
 
 
                 if (blurayInfo.Chapters != null)
                 if (blurayInfo.Chapters != null)
                 {
                 {
-                    chapters.Clear();
-
-                    chapters.AddRange(blurayInfo.Chapters.Select(c => new ChapterInfo
+                    double[] brChapter = blurayInfo.Chapters;
+                    chapters = new ChapterInfo[brChapter.Length];
+                    for (int i = 0; i < brChapter.Length; i++)
                     {
                     {
-                        StartPositionTicks = TimeSpan.FromSeconds(c).Ticks
-
-                    }));
+                        chapters[i] = new ChapterInfo
+                        {
+                            StartPositionTicks = TimeSpan.FromSeconds(brChapter[i]).Ticks
+                        };
+                    }
                 }
                 }
 
 
                 videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video);
                 videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video);
@@ -495,17 +509,17 @@ namespace MediaBrowser.Providers.MediaInfo
             var libraryOptions = _libraryManager.GetLibraryOptions(video);
             var libraryOptions = _libraryManager.GetLibraryOptions(video);
 
 
             string[] subtitleDownloadLanguages;
             string[] subtitleDownloadLanguages;
-            bool SkipIfEmbeddedSubtitlesPresent;
-            bool SkipIfAudioTrackMatches;
-            bool RequirePerfectMatch;
+            bool skipIfEmbeddedSubtitlesPresent;
+            bool skipIfAudioTrackMatches;
+            bool requirePerfectMatch;
             bool enabled;
             bool enabled;
 
 
             if (libraryOptions.SubtitleDownloadLanguages == null)
             if (libraryOptions.SubtitleDownloadLanguages == null)
             {
             {
                 subtitleDownloadLanguages = subtitleOptions.DownloadLanguages;
                 subtitleDownloadLanguages = subtitleOptions.DownloadLanguages;
-                SkipIfEmbeddedSubtitlesPresent = subtitleOptions.SkipIfEmbeddedSubtitlesPresent;
-                SkipIfAudioTrackMatches = subtitleOptions.SkipIfAudioTrackMatches;
-                RequirePerfectMatch = subtitleOptions.RequirePerfectMatch;
+                skipIfEmbeddedSubtitlesPresent = subtitleOptions.SkipIfEmbeddedSubtitlesPresent;
+                skipIfAudioTrackMatches = subtitleOptions.SkipIfAudioTrackMatches;
+                requirePerfectMatch = subtitleOptions.RequirePerfectMatch;
                 enabled = (subtitleOptions.DownloadEpisodeSubtitles &&
                 enabled = (subtitleOptions.DownloadEpisodeSubtitles &&
                 video is Episode) ||
                 video is Episode) ||
                 (subtitleOptions.DownloadMovieSubtitles &&
                 (subtitleOptions.DownloadMovieSubtitles &&
@@ -514,9 +528,9 @@ namespace MediaBrowser.Providers.MediaInfo
             else
             else
             {
             {
                 subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages;
                 subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages;
-                SkipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent;
-                SkipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches;
-                RequirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch;
+                skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent;
+                skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches;
+                requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch;
                 enabled = true;
                 enabled = true;
             }
             }
 
 
@@ -526,9 +540,9 @@ namespace MediaBrowser.Providers.MediaInfo
                     _subtitleManager)
                     _subtitleManager)
                     .DownloadSubtitles(video,
                     .DownloadSubtitles(video,
                     currentStreams.Concat(externalSubtitleStreams).ToList(),
                     currentStreams.Concat(externalSubtitleStreams).ToList(),
-                    SkipIfEmbeddedSubtitlesPresent,
-                    SkipIfAudioTrackMatches,
-                    RequirePerfectMatch,
+                    skipIfEmbeddedSubtitlesPresent,
+                    skipIfAudioTrackMatches,
+                    requirePerfectMatch,
                     subtitleDownloadLanguages,
                     subtitleDownloadLanguages,
                     libraryOptions.DisabledSubtitleFetchers,
                     libraryOptions.DisabledSubtitleFetchers,
                     libraryOptions.SubtitleFetcherOrder,
                     libraryOptions.SubtitleFetcherOrder,
@@ -547,43 +561,45 @@ namespace MediaBrowser.Providers.MediaInfo
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// The dummy chapter duration
-        /// </summary>
-        private readonly long _dummyChapterDuration = TimeSpan.FromMinutes(5).Ticks;
-
-        /// <summary>
-        /// Adds the dummy chapters.
+        /// Creates dummy chapters.
         /// </summary>
         /// </summary>
         /// <param name="video">The video.</param>
         /// <param name="video">The video.</param>
-        /// <param name="chapters">The chapters.</param>
-        private void AddDummyChapters(Video video, List<ChapterInfo> chapters)
+        /// <return>An array of dummy chapters.</returns>
+        private ChapterInfo[] CreateDummyChapters(Video video)
         {
         {
             var runtime = video.RunTimeTicks ?? 0;
             var runtime = video.RunTimeTicks ?? 0;
 
 
             if (runtime < 0)
             if (runtime < 0)
             {
             {
-                throw new ArgumentException(string.Format("{0} has invalid runtime of {1}", video.Name, runtime));
+                throw new ArgumentException(
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        "{0} has invalid runtime of {1}",
+                        video.Name,
+                        runtime));
             }
             }
 
 
             if (runtime < _dummyChapterDuration)
             if (runtime < _dummyChapterDuration)
             {
             {
-                return;
+                return Array.Empty<ChapterInfo>();
             }
             }
 
 
-            long currentChapterTicks = 0;
-            var index = 1;
-
             // Limit to 100 chapters just in case there's some incorrect metadata here
             // Limit to 100 chapters just in case there's some incorrect metadata here
-            while (currentChapterTicks < runtime && index < 100)
+            int chapterCount = (int)Math.Min(runtime / _dummyChapterDuration, 100);
+            var chapters = new ChapterInfo[chapterCount];
+
+            long currentChapterTicks = 0;
+            for (int i = 0; i < chapterCount; i++)
             {
             {
-                chapters.Add(new ChapterInfo
+                chapters[i] = new ChapterInfo
                 {
                 {
                     StartPositionTicks = currentChapterTicks
                     StartPositionTicks = currentChapterTicks
-                });
+                };
 
 
-                index++;
                 currentChapterTicks += _dummyChapterDuration;
                 currentChapterTicks += _dummyChapterDuration;
             }
             }
+
+            return chapters;
         }
         }
 
 
         private string[] FetchFromDvdLib(Video item)
         private string[] FetchFromDvdLib(Video item)

+ 7 - 7
MediaBrowser.Providers/Tmdb/Movies/TmdbMovieProvider.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Net;
 using System.Net;
+using System.Net.Http;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
@@ -36,7 +37,6 @@ namespace MediaBrowser.Providers.Tmdb.Movies
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly IServerConfigurationManager _configurationManager;
         private readonly IServerConfigurationManager _configurationManager;
         private readonly ILogger _logger;
         private readonly ILogger _logger;
-        private readonly ILocalizationManager _localization;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
         private readonly IApplicationHost _appHost;
         private readonly IApplicationHost _appHost;
 
 
@@ -48,7 +48,6 @@ namespace MediaBrowser.Providers.Tmdb.Movies
             IFileSystem fileSystem,
             IFileSystem fileSystem,
             IServerConfigurationManager configurationManager,
             IServerConfigurationManager configurationManager,
             ILogger<TmdbMovieProvider> logger,
             ILogger<TmdbMovieProvider> logger,
-            ILocalizationManager localization,
             ILibraryManager libraryManager,
             ILibraryManager libraryManager,
             IApplicationHost appHost)
             IApplicationHost appHost)
         {
         {
@@ -57,7 +56,6 @@ namespace MediaBrowser.Providers.Tmdb.Movies
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
             _configurationManager = configurationManager;
             _configurationManager = configurationManager;
             _logger = logger;
             _logger = logger;
-            _localization = localization;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
             _appHost = appHost;
             _appHost = appHost;
             Current = this;
             Current = this;
@@ -409,15 +407,15 @@ namespace MediaBrowser.Providers.Tmdb.Movies
 
 
         private static long _lastRequestTicks;
         private static long _lastRequestTicks;
         // The limit is 40 requests per 10 seconds
         // The limit is 40 requests per 10 seconds
-        private static int requestIntervalMs = 300;
+        private const int RequestIntervalMs = 300;
 
 
         /// <summary>
         /// <summary>
         /// Gets the movie db response.
         /// Gets the movie db response.
         /// </summary>
         /// </summary>
         internal async Task<HttpResponseInfo> GetMovieDbResponse(HttpRequestOptions options)
         internal async Task<HttpResponseInfo> GetMovieDbResponse(HttpRequestOptions options)
         {
         {
-            var delayTicks = (requestIntervalMs * 10000) - (DateTime.UtcNow.Ticks - _lastRequestTicks);
-            var delayMs = Math.Min(delayTicks / 10000, requestIntervalMs);
+            var delayTicks = (RequestIntervalMs * 10000) - (DateTime.UtcNow.Ticks - _lastRequestTicks);
+            var delayMs = Math.Min(delayTicks / 10000, RequestIntervalMs);
 
 
             if (delayMs > 0)
             if (delayMs > 0)
             {
             {
@@ -430,11 +428,13 @@ namespace MediaBrowser.Providers.Tmdb.Movies
             options.BufferContent = true;
             options.BufferContent = true;
             options.UserAgent = _appHost.ApplicationUserAgent;
             options.UserAgent = _appHost.ApplicationUserAgent;
 
 
-            return await _httpClient.SendAsync(options, "GET").ConfigureAwait(false);
+            return await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false);
         }
         }
 
 
+        /// <inheritdoc />
         public int Order => 1;
         public int Order => 1;
 
 
+        /// <inheritdoc />
         public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
         public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
         {
         {
             return _httpClient.GetResponse(new HttpRequestOptions
             return _httpClient.GetResponse(new HttpRequestOptions

+ 36 - 10
MediaBrowser.WebDashboard/Api/DashboardService.cs

@@ -12,12 +12,14 @@ using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Plugins;
 using MediaBrowser.Model.Plugins;
 using MediaBrowser.Model.Services;
 using MediaBrowser.Model.Services;
+using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
 namespace MediaBrowser.WebDashboard.Api
 namespace MediaBrowser.WebDashboard.Api
@@ -102,6 +104,7 @@ namespace MediaBrowser.WebDashboard.Api
         /// <value>The HTTP result factory.</value>
         /// <value>The HTTP result factory.</value>
         private readonly IHttpResultFactory _resultFactory;
         private readonly IHttpResultFactory _resultFactory;
         private readonly IServerApplicationHost _appHost;
         private readonly IServerApplicationHost _appHost;
+        private readonly IConfiguration _appConfig;
         private readonly IServerConfigurationManager _serverConfigurationManager;
         private readonly IServerConfigurationManager _serverConfigurationManager;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly IResourceFileManager _resourceFileManager;
         private readonly IResourceFileManager _resourceFileManager;
@@ -111,6 +114,7 @@ namespace MediaBrowser.WebDashboard.Api
         /// </summary>
         /// </summary>
         /// <param name="logger">The logger.</param>
         /// <param name="logger">The logger.</param>
         /// <param name="appHost">The application host.</param>
         /// <param name="appHost">The application host.</param>
+        /// <param name="appConfig">The application configuration.</param>
         /// <param name="resourceFileManager">The resource file manager.</param>
         /// <param name="resourceFileManager">The resource file manager.</param>
         /// <param name="serverConfigurationManager">The server configuration manager.</param>
         /// <param name="serverConfigurationManager">The server configuration manager.</param>
         /// <param name="fileSystem">The file system.</param>
         /// <param name="fileSystem">The file system.</param>
@@ -118,6 +122,7 @@ namespace MediaBrowser.WebDashboard.Api
         public DashboardService(
         public DashboardService(
             ILogger<DashboardService> logger,
             ILogger<DashboardService> logger,
             IServerApplicationHost appHost,
             IServerApplicationHost appHost,
+            IConfiguration appConfig,
             IResourceFileManager resourceFileManager,
             IResourceFileManager resourceFileManager,
             IServerConfigurationManager serverConfigurationManager,
             IServerConfigurationManager serverConfigurationManager,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
@@ -125,6 +130,7 @@ namespace MediaBrowser.WebDashboard.Api
         {
         {
             _logger = logger;
             _logger = logger;
             _appHost = appHost;
             _appHost = appHost;
+            _appConfig = appConfig;
             _resourceFileManager = resourceFileManager;
             _resourceFileManager = resourceFileManager;
             _serverConfigurationManager = serverConfigurationManager;
             _serverConfigurationManager = serverConfigurationManager;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
@@ -138,20 +144,30 @@ namespace MediaBrowser.WebDashboard.Api
         public IRequest Request { get; set; }
         public IRequest Request { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Gets the path for the web interface.
+        /// Gets the path of the directory containing the static web interface content, or null if the server is not
+        /// hosting the web client.
         /// </summary>
         /// </summary>
-        /// <value>The path for the web interface.</value>
-        public string DashboardUIPath
+        public string DashboardUIPath => GetDashboardUIPath(_appConfig, _serverConfigurationManager);
+
+        /// <summary>
+        /// Gets the path of the directory containing the static web interface content.
+        /// </summary>
+        /// <param name="appConfig">The app configuration.</param>
+        /// <param name="serverConfigManager">The server configuration manager.</param>
+        /// <returns>The directory path, or null if the server is not hosting the web client.</returns>
+        public static string GetDashboardUIPath(IConfiguration appConfig, IServerConfigurationManager serverConfigManager)
         {
         {
-            get
+            if (!appConfig.HostWebClient())
             {
             {
-                if (!string.IsNullOrEmpty(_serverConfigurationManager.Configuration.DashboardSourcePath))
-                {
-                    return _serverConfigurationManager.Configuration.DashboardSourcePath;
-                }
+                return null;
+            }
 
 
-                return _serverConfigurationManager.ApplicationPaths.WebPath;
+            if (!string.IsNullOrEmpty(serverConfigManager.Configuration.DashboardSourcePath))
+            {
+                return serverConfigManager.Configuration.DashboardSourcePath;
             }
             }
+
+            return serverConfigManager.ApplicationPaths.WebPath;
         }
         }
 
 
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
@@ -209,7 +225,7 @@ namespace MediaBrowser.WebDashboard.Api
                     return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => Task.FromResult(stream));
                     return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => Task.FromResult(stream));
                 }
                 }
 
 
-                return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => GetPackageCreator(DashboardUIPath).ModifyHtml("dummy.html", stream, null, _appHost.ApplicationVersionString, null));
+                return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => PackageCreator.ModifyHtml(false, stream, null, _appHost.ApplicationVersionString, null));
             }
             }
 
 
             throw new ResourceNotFoundException();
             throw new ResourceNotFoundException();
@@ -307,6 +323,11 @@ namespace MediaBrowser.WebDashboard.Api
         /// <returns>System.Object.</returns>
         /// <returns>System.Object.</returns>
         public async Task<object> Get(GetDashboardResource request)
         public async Task<object> Get(GetDashboardResource request)
         {
         {
+            if (!_appConfig.HostWebClient() || DashboardUIPath == null)
+            {
+                throw new ResourceNotFoundException();
+            }
+
             var path = request.ResourceName;
             var path = request.ResourceName;
 
 
             var contentType = MimeTypes.GetMimeType(path);
             var contentType = MimeTypes.GetMimeType(path);
@@ -378,6 +399,11 @@ namespace MediaBrowser.WebDashboard.Api
 
 
         public async Task<object> Get(GetDashboardPackage request)
         public async Task<object> Get(GetDashboardPackage request)
         {
         {
+            if (!_appConfig.HostWebClient() || DashboardUIPath == null)
+            {
+                throw new ResourceNotFoundException();
+            }
+
             var mode = request.Mode;
             var mode = request.Mode;
 
 
             var inputPath = string.IsNullOrWhiteSpace(mode) ?
             var inputPath = string.IsNullOrWhiteSpace(mode) ?

+ 16 - 6
MediaBrowser.WebDashboard/Api/PackageCreator.cs

@@ -31,7 +31,8 @@ namespace MediaBrowser.WebDashboard.Api
 
 
             if (resourceStream != null && IsCoreHtml(virtualPath))
             if (resourceStream != null && IsCoreHtml(virtualPath))
             {
             {
-                resourceStream = await ModifyHtml(virtualPath, resourceStream, mode, appVersion, localizationCulture).ConfigureAwait(false);
+                bool isMainIndexPage = string.Equals(virtualPath, "index.html", StringComparison.OrdinalIgnoreCase);
+                resourceStream = await ModifyHtml(isMainIndexPage, resourceStream, mode, appVersion, localizationCulture).ConfigureAwait(false);
             }
             }
 
 
             return resourceStream;
             return resourceStream;
@@ -47,16 +48,25 @@ namespace MediaBrowser.WebDashboard.Api
             return string.Equals(Path.GetExtension(path), ".html", StringComparison.OrdinalIgnoreCase);
             return string.Equals(Path.GetExtension(path), ".html", StringComparison.OrdinalIgnoreCase);
         }
         }
 
 
-        // Modifies the HTML by adding common meta tags, css and js.
-        public async Task<Stream> ModifyHtml(
-            string path,
+        /// <summary>
+        /// Modifies the source HTML stream by adding common meta tags, css and js.
+        /// </summary>
+        /// <param name="isMainIndexPage">True if the stream contains content for the main index page.</param>
+        /// <param name="sourceStream">The stream whose content should be modified.</param>
+        /// <param name="mode">The client mode ('cordova', 'android', etc).</param>
+        /// <param name="appVersion">The application version.</param>
+        /// <param name="localizationCulture">The localization culture.</param>
+        /// <returns>
+        /// A task that represents the async operation to read and modify the input stream.
+        /// The task result contains a stream containing the modified HTML content.
+        /// </returns>
+        public static async Task<Stream> ModifyHtml(
+            bool isMainIndexPage,
             Stream sourceStream,
             Stream sourceStream,
             string mode,
             string mode,
             string appVersion,
             string appVersion,
             string localizationCulture)
             string localizationCulture)
         {
         {
-            var isMainIndexPage = string.Equals(path, "index.html", StringComparison.OrdinalIgnoreCase);
-
             string html;
             string html;
             using (var reader = new StreamReader(sourceStream, Encoding.UTF8))
             using (var reader = new StreamReader(sourceStream, Encoding.UTF8))
             {
             {

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

@@ -12,7 +12,7 @@
     <PackageReference Include="AutoFixture.AutoMoq" Version="4.11.0" />
     <PackageReference Include="AutoFixture.AutoMoq" Version="4.11.0" />
     <PackageReference Include="AutoFixture.Xunit2" Version="4.11.0" />
     <PackageReference Include="AutoFixture.Xunit2" Version="4.11.0" />
     <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.3" />
     <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.3" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="coverlet.collector" Version="1.2.0" />
     <PackageReference Include="coverlet.collector" Version="1.2.0" />

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

@@ -8,7 +8,7 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="coverlet.collector" Version="1.2.0" />
     <PackageReference Include="coverlet.collector" Version="1.2.0" />

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

@@ -8,7 +8,7 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="coverlet.collector" Version="1.2.0" />
     <PackageReference Include="coverlet.collector" Version="1.2.0" />

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

@@ -14,7 +14,7 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="coverlet.collector" Version="1.2.0" />
     <PackageReference Include="coverlet.collector" Version="1.2.0" />

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

@@ -7,7 +7,7 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="coverlet.collector" Version="1.2.0" />
     <PackageReference Include="coverlet.collector" Version="1.2.0" />